Skip to content

Index

Main Lutris package

__version__ special

api

Functions to interact with the Lutris REST API

API_KEY_FILE_PATH

USER_ICON_FILE_PATH

USER_INFO_FILE_PATH

connect(username, password)

Connect to the Lutris API

Source code in lutris/api.py
def connect(username, password):
    """Connect to the Lutris API"""
    login_url = settings.SITE_URL + "/api/accounts/token"
    credentials = {"username": username, "password": password}
    try:
        response = requests.post(url=login_url, data=credentials, timeout=10)
        response.raise_for_status()
        json_dict = response.json()
        if "token" in json_dict:
            token = json_dict["token"]
            with open(API_KEY_FILE_PATH, "w", encoding='utf-8') as token_file:
                token_file.write("%s:%s" % (username, token))
            get_user_info()
            return token
    except (requests.RequestException, requests.ConnectionError, requests.HTTPError, requests.TooManyRedirects,
            requests.Timeout) as ex:
        logger.error("Unable to connect to server (%s): %s", login_url, ex)
        return False

disconnect()

Removes the API token, disconnecting the user

Source code in lutris/api.py
def disconnect():
    """Removes the API token, disconnecting the user"""
    for file_path in [API_KEY_FILE_PATH, USER_INFO_FILE_PATH]:
        if system.path_exists(file_path):
            os.remove(file_path)

get_api_games(game_slugs=None, page=1, service=None)

Return all games from the Lutris API matching the given game slugs

Source code in lutris/api.py
def get_api_games(game_slugs=None, page=1, service=None):
    """Return all games from the Lutris API matching the given game slugs"""
    if service:
        response_data = get_game_service_api_page(service, game_slugs)
    else:
        response_data = get_game_api_page(game_slugs)

    if not response_data:
        return []
    results = response_data.get("results", [])
    while response_data.get("next"):
        page_match = re.search(r"page=(\d+)", response_data["next"])
        if page_match:
            next_page = page_match.group(1)
        else:
            logger.error("No page found in %s", response_data["next"])
            break
        if service:
            response_data = get_game_service_api_page(service, game_slugs, page=next_page)
        else:
            response_data = get_game_api_page(game_slugs, page=next_page)
        if not response_data:
            logger.warning("Unable to get response for page %s", next_page)
            break
        results += response_data.get("results")
    return results

get_bundle(bundle)

Retrieve a lutris bundle from the API

Source code in lutris/api.py
def get_bundle(bundle):
    """Retrieve a lutris bundle from the API"""
    url = "/api/bundles/%s" % bundle
    response = http.Request(settings.SITE_URL + url, headers={"Content-Type": "application/json"})
    try:
        response.get()
    except http.HTTPError as ex:
        logger.error("Unable to get bundle from API: %s", ex)
        return None
    response_data = response.json
    return response_data.get("games", [])

get_game_api_page(game_slugs, page=1)

Read a single page of games from the API and return the response

Parameters:

Name Type Description Default
game_ids list

list of game slugs

required
page str

Page of results to get

1
Source code in lutris/api.py
def get_game_api_page(game_slugs, page=1):
    """Read a single page of games from the API and return the response

    Args:
        game_ids (list): list of game slugs
        page (str): Page of results to get
    """
    url = settings.SITE_URL + "/api/games"
    if int(page) > 1:
        url += "?page={}".format(page)
    if not game_slugs:
        return []
    payload = json.dumps({"games": game_slugs, "page": page}).encode("utf-8")
    return get_http_response(url, payload)

get_game_installers(game_slug, revision=None)

Get installers for a single game

Source code in lutris/api.py
def get_game_installers(game_slug, revision=None):
    """Get installers for a single game"""
    if not game_slug:
        raise ValueError("No game_slug provided. Can't query an installer")
    if revision:
        installer_url = settings.INSTALLER_REVISION_URL % (game_slug, revision)
    else:
        installer_url = settings.INSTALLER_URL % game_slug

    logger.debug("Fetching installer %s", installer_url)
    request = http.Request(installer_url)
    request.get()
    response = request.json
    if response is None:
        raise RuntimeError("Couldn't get installer at %s" % installer_url)

    if not revision:
        return response["results"]
    # Revision requests return a single installer
    return [response]

get_game_service_api_page(service, appids, page=1)

Get matching Lutris games from a list of appids from a given service

Source code in lutris/api.py
def get_game_service_api_page(service, appids, page=1):
    """Get matching Lutris games from a list of appids from a given service"""
    url = settings.SITE_URL + "/api/games/service/%s" % service
    if int(page) > 1:
        url += "?page={}".format(page)
    if not appids:
        return []
    payload = json.dumps({"appids": appids}).encode("utf-8")
    return get_http_response(url, payload)

get_http_response(url, payload)

Source code in lutris/api.py
def get_http_response(url, payload):
    response = http.Request(url, headers={"Content-Type": "application/json"})
    try:
        response.get(data=payload)
    except http.HTTPError as ex:
        logger.error("Unable to get games from API: %s", ex)
        return None
    if response.status_code != 200:
        logger.error("API call failed: %s", response.status_code)
        return None
    return response.json

get_runners(runner_name)

Return the available runners for a given runner name

Source code in lutris/api.py
def get_runners(runner_name):
    """Return the available runners for a given runner name"""
    api_url = settings.SITE_URL + "/api/runners/" + runner_name
    response = http.Request(api_url).get()
    return response.json

get_user_info()

Retrieves the user info to cache it locally

Source code in lutris/api.py
def get_user_info():
    """Retrieves the user info to cache it locally"""
    credentials = read_api_key()
    if not credentials:
        return
    url = settings.SITE_URL + "/api/users/me"
    request = http.Request(url, headers={"Authorization": "Token " + credentials["token"]})
    response = request.get()
    account_info = response.json
    if not account_info:
        logger.warning("Unable to fetch user info for %s", credentials["username"])
    with open(USER_INFO_FILE_PATH, "w", encoding='utf-8') as token_file:
        json.dump(account_info, token_file, indent=2)

parse_installer_url(url)

Parses lutris: urls, extracting any info necessary to install or run a game.

Source code in lutris/api.py
def parse_installer_url(url):
    """
    Parses `lutris:` urls, extracting any info necessary to install or run a game.
    """
    action = None
    try:
        parsed_url = urllib.parse.urlparse(url, scheme="lutris")
    except Exception:  # pylint: disable=broad-except
        logger.warning("Unable to parse url %s", url)
        return False
    if parsed_url.scheme != "lutris":
        return False
    url_path = parsed_url.path
    if not url_path:
        return False
    # urlparse can't parse if the path only contain numbers
    # workaround to remove the scheme manually:
    if url_path.startswith("lutris:"):
        url_path = url_path[7:]

    url_parts = url_path.split("/")
    if len(url_parts) == 2:
        action = url_parts[0]
        game_slug = url_parts[1]
    elif len(url_parts) == 1:
        game_slug = url_parts[0]
    else:
        raise ValueError("Invalid lutris url %s" % url)

    # To link to service games, format a slug like <service>:<appid>
    if ":" in game_slug:
        service, appid = game_slug.split(":", maxsplit=1)
    else:
        service, appid = "", ""

    revision = None
    if parsed_url.query:
        query = dict(urllib.parse.parse_qsl(parsed_url.query))
        revision = query.get("revision")
    return {
        "game_slug": game_slug,
        "revision": revision,
        "action": action,
        "service": service,
        "appid": appid
    }

read_api_key()

Read the API token from disk

Source code in lutris/api.py
def read_api_key():
    """Read the API token from disk"""
    if not system.path_exists(API_KEY_FILE_PATH):
        return None
    with open(API_KEY_FILE_PATH, "r", encoding='utf-8') as token_file:
        api_string = token_file.read()
    try:
        username, token = api_string.split(":")
    except ValueError:
        logger.error("Unable to read Lutris token in %s", API_KEY_FILE_PATH)
        return None
    return {"token": token, "username": username}

search_games(query)

Source code in lutris/api.py
def search_games(query):
    if not query:
        return {}
    query = query.lower().strip()[:255]
    url = "/api/games?%s" % urllib.parse.urlencode({"search": query, "with-installers": True})
    response = http.Request(settings.SITE_URL + url, headers={"Content-Type": "application/json"})
    try:
        response.get()
    except http.HTTPError as ex:
        logger.error("Unable to get games from API: %s", ex)
        return {}
    return response.json

cache

Module for handling the PGA cache

get_cache_path()

Return the path of the PGA cache

Source code in lutris/cache.py
def get_cache_path():
    """Return the path of the PGA cache"""
    pga_cache_path = settings.read_setting("pga_cache_path")
    if pga_cache_path:
        return os.path.expanduser(pga_cache_path)
    return None

save_cache_path(path)

Saves the PGA cache path to the settings

Source code in lutris/cache.py
def save_cache_path(path):
    """Saves the PGA cache path to the settings"""
    settings.write_setting("pga_cache_path", path)

save_to_cache(source, destination)

Copy a file or folder to the cache

Source code in lutris/cache.py
def save_to_cache(source, destination):
    """Copy a file or folder to the cache"""
    if not source:
        raise ValueError("Missing source")
    if os.path.dirname(source) == destination:
        logger.info("Skipping caching of %s, already cached in %s", source, destination)
        return
    if os.path.isdir(source):
        # Copy folder recursively
        merge_folders(source, destination)
    else:
        shutil.copy(source, destination)
    logger.debug("Cached %s to %s", source, destination)

command

Threading module, used to launch games while monitoring them.

WRAPPER_SCRIPT

MonitoredCommand

Exexcutes a commmand while keeping track of its state

Source code in lutris/command.py
class MonitoredCommand:

    """Exexcutes a commmand while keeping track of its state"""

    fallback_cwd = "/tmp"

    def __init__(
        self,
        command,
        runner=None,
        env=None,
        term=None,
        cwd=None,
        include_processes=None,
        exclude_processes=None,
        log_buffer=None,
        title=None,
    ):  # pylint: disable=too-many-arguments
        self.ready_state = True
        self.env = self.get_environment(env)

        self.accepted_return_code = "0"

        self.command = command
        self.runner = runner
        self.stop_func = lambda: True
        self.game_process = None
        self.prevent_on_stop = False
        self.return_code = None
        self.terminal = term
        self.is_running = True
        self.error = None
        self.log_handlers = [
            self.log_handler_stdout,
            self.log_handler_console_output,
        ]
        self.set_log_buffer(log_buffer)
        self.stdout_monitor = None
        self.include_processes = include_processes or []
        self.exclude_processes = exclude_processes or []

        self.cwd = self.get_cwd(cwd)

        self._stdout = io.StringIO()

        self._title = title if title else command[0]

    @property
    def stdout(self):
        return self._stdout.getvalue()

    def get_wrapper_command(self):
        """Return launch arguments for the wrapper script"""
        wrapper_command = [
            WRAPPER_SCRIPT,
            self._title,
            str(len(self.include_processes)),
            str(len(self.exclude_processes)),
        ] + self.include_processes + self.exclude_processes
        if not self.terminal:
            return wrapper_command + self.command

        terminal_path = system.find_executable(self.terminal)
        if not terminal_path:
            raise RuntimeError("Couldn't find terminal %s" % self.terminal)
        script_path = get_terminal_script(self.command, self.cwd, self.env)
        return wrapper_command + [terminal_path, "-e", script_path]

    def set_log_buffer(self, log_buffer):
        """Attach a TextBuffer to this command enables the buffer handler"""
        if not log_buffer:
            return
        self.log_buffer = log_buffer
        if self.log_handler_buffer not in self.log_handlers:
            self.log_handlers.append(self.log_handler_buffer)

    def get_cwd(self, cwd):
        """Return the current working dir of the game"""
        if not cwd:
            cwd = self.runner.working_dir if self.runner else None
        return os.path.expanduser(cwd or "~")

    @staticmethod
    def get_environment(user_env):
        """Process the user provided environment variables for use as self.env"""
        env = user_env or {}
        # not clear why this needs to be added, the path is already added in
        # the wrappper script.
        env['PYTHONPATH'] = ':'.join(sys.path)
        # Drop bad values of environment keys, those will confuse the Python
        # interpreter.
        env["LUTRIS_GAME_UUID"] = str(uuid.uuid4())
        return {key: value for key, value in env.items() if "=" not in key}

    def get_child_environment(self):
        """Returns the calculated environment for the child process."""
        env = os.environ.copy()
        env.update(self.env)
        return env

    def start(self):
        """Run the thread."""
        for key, value in self.env.items():
            logger.debug("%s=\"%s\"", key, value)
        wrapper_command = self.get_wrapper_command()
        env = self.get_child_environment()
        self.game_process = self.execute_process(wrapper_command, env)

        if not self.game_process:
            logger.error("No game process available")
            return

        GLib.child_watch_add(self.game_process.pid, self.on_stop)

        # make stdout nonblocking.
        fileno = self.game_process.stdout.fileno()
        fcntl.fcntl(fileno, fcntl.F_SETFL, fcntl.fcntl(fileno, fcntl.F_GETFL) | os.O_NONBLOCK)

        self.stdout_monitor = GLib.io_add_watch(
            self.game_process.stdout,
            GLib.IO_IN | GLib.IO_HUP,
            self.on_stdout_output,
        )

    def log_handler_stdout(self, line):
        """Add the line to this command's stdout attribute"""
        self._stdout.write(line)

    def log_handler_buffer(self, line):
        """Add the line to the associated LogBuffer object"""
        self.log_buffer.insert(self.log_buffer.get_end_iter(), line, -1)

    def log_handler_console_output(self, line):  # pylint: disable=no-self-use
        """Print the line to stdout"""
        with contextlib.suppress(BlockingIOError):
            sys.stdout.write(line)
            sys.stdout.flush()

    def get_return_code(self):
        """Get the return code from the file written by the wrapper"""
        return_code_path = "/tmp/lutris-%s" % self.env["LUTRIS_GAME_UUID"]
        if os.path.exists(return_code_path):
            with open(return_code_path, encoding='utf-8') as return_code_file:
                return_code = return_code_file.read()
            os.unlink(return_code_path)
        else:
            return_code = ''
            logger.warning("No file %s", return_code_path)
        return return_code

    def on_stop(self, pid, _user_data):
        """Callback registered on game process termination"""
        if self.prevent_on_stop:  # stop() already in progress
            return False
        self.game_process.wait()
        self.return_code = self.get_return_code()
        self.is_running = False
        logger.debug("Process %s has terminated with code %s", pid, self.return_code)
        resume_stop = self.stop()
        if not resume_stop:
            logger.info("Full shutdown prevented")
            return False
        return False

    def on_stdout_output(self, stdout, condition):
        """Called by the stdout monitor to dispatch output to log handlers"""
        if condition == GLib.IO_HUP:
            self.stdout_monitor = None
            return False
        if not self.is_running:
            return False
        try:
            line = stdout.read(262144).decode("utf-8", errors="ignore")
        except ValueError:
            # file_desc might be closed
            return True
        if "winemenubuilder.exe" in line:
            return True
        for log_handler in self.log_handlers:
            log_handler(line)
        return True

    def execute_process(self, command, env=None):
        """Execute and return a subprocess"""
        if self.cwd and not system.path_exists(self.cwd):
            try:
                os.makedirs(self.cwd)
            except OSError:
                logger.error("Failed to create working directory, falling back to %s", self.fallback_cwd)
                self.cwd = "/tmp"
        try:
            return subprocess.Popen(  # pylint: disable=consider-using-with
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                cwd=self.cwd,
                env=env,
            )
        except OSError as ex:
            logger.exception("Failed to execute %s: %s", " ".join(command), ex)
            self.error = ex.strerror

    def stop(self):
        """Stops the current game process and cleans up the instance"""
        # Prevent stop() being called again by the process exiting
        self.prevent_on_stop = True

        try:
            self.game_process.terminate()
        except ProcessLookupError:
            # process already dead.
            pass

        resume_stop = self.stop_func()
        if not resume_stop:
            logger.warning("Stop execution halted by demand of stop_func")
            return False

        if self.stdout_monitor:
            GLib.source_remove(self.stdout_monitor)
            self.stdout_monitor = None

        self.is_running = False
        self.ready_state = False
        return True

fallback_cwd

stdout property readonly

__init__(self, command, runner=None, env=None, term=None, cwd=None, include_processes=None, exclude_processes=None, log_buffer=None, title=None) special

Source code in lutris/command.py
def __init__(
    self,
    command,
    runner=None,
    env=None,
    term=None,
    cwd=None,
    include_processes=None,
    exclude_processes=None,
    log_buffer=None,
    title=None,
):  # pylint: disable=too-many-arguments
    self.ready_state = True
    self.env = self.get_environment(env)

    self.accepted_return_code = "0"

    self.command = command
    self.runner = runner
    self.stop_func = lambda: True
    self.game_process = None
    self.prevent_on_stop = False
    self.return_code = None
    self.terminal = term
    self.is_running = True
    self.error = None
    self.log_handlers = [
        self.log_handler_stdout,
        self.log_handler_console_output,
    ]
    self.set_log_buffer(log_buffer)
    self.stdout_monitor = None
    self.include_processes = include_processes or []
    self.exclude_processes = exclude_processes or []

    self.cwd = self.get_cwd(cwd)

    self._stdout = io.StringIO()

    self._title = title if title else command[0]

execute_process(self, command, env=None)

Execute and return a subprocess

Source code in lutris/command.py
def execute_process(self, command, env=None):
    """Execute and return a subprocess"""
    if self.cwd and not system.path_exists(self.cwd):
        try:
            os.makedirs(self.cwd)
        except OSError:
            logger.error("Failed to create working directory, falling back to %s", self.fallback_cwd)
            self.cwd = "/tmp"
    try:
        return subprocess.Popen(  # pylint: disable=consider-using-with
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            cwd=self.cwd,
            env=env,
        )
    except OSError as ex:
        logger.exception("Failed to execute %s: %s", " ".join(command), ex)
        self.error = ex.strerror

get_child_environment(self)

Returns the calculated environment for the child process.

Source code in lutris/command.py
def get_child_environment(self):
    """Returns the calculated environment for the child process."""
    env = os.environ.copy()
    env.update(self.env)
    return env

get_cwd(self, cwd)

Return the current working dir of the game

Source code in lutris/command.py
def get_cwd(self, cwd):
    """Return the current working dir of the game"""
    if not cwd:
        cwd = self.runner.working_dir if self.runner else None
    return os.path.expanduser(cwd or "~")

get_environment(user_env) staticmethod

Process the user provided environment variables for use as self.env

Source code in lutris/command.py
@staticmethod
def get_environment(user_env):
    """Process the user provided environment variables for use as self.env"""
    env = user_env or {}
    # not clear why this needs to be added, the path is already added in
    # the wrappper script.
    env['PYTHONPATH'] = ':'.join(sys.path)
    # Drop bad values of environment keys, those will confuse the Python
    # interpreter.
    env["LUTRIS_GAME_UUID"] = str(uuid.uuid4())
    return {key: value for key, value in env.items() if "=" not in key}

get_return_code(self)

Get the return code from the file written by the wrapper

Source code in lutris/command.py
def get_return_code(self):
    """Get the return code from the file written by the wrapper"""
    return_code_path = "/tmp/lutris-%s" % self.env["LUTRIS_GAME_UUID"]
    if os.path.exists(return_code_path):
        with open(return_code_path, encoding='utf-8') as return_code_file:
            return_code = return_code_file.read()
        os.unlink(return_code_path)
    else:
        return_code = ''
        logger.warning("No file %s", return_code_path)
    return return_code

get_wrapper_command(self)

Return launch arguments for the wrapper script

Source code in lutris/command.py
def get_wrapper_command(self):
    """Return launch arguments for the wrapper script"""
    wrapper_command = [
        WRAPPER_SCRIPT,
        self._title,
        str(len(self.include_processes)),
        str(len(self.exclude_processes)),
    ] + self.include_processes + self.exclude_processes
    if not self.terminal:
        return wrapper_command + self.command

    terminal_path = system.find_executable(self.terminal)
    if not terminal_path:
        raise RuntimeError("Couldn't find terminal %s" % self.terminal)
    script_path = get_terminal_script(self.command, self.cwd, self.env)
    return wrapper_command + [terminal_path, "-e", script_path]

log_handler_buffer(self, line)

Add the line to the associated LogBuffer object

Source code in lutris/command.py
def log_handler_buffer(self, line):
    """Add the line to the associated LogBuffer object"""
    self.log_buffer.insert(self.log_buffer.get_end_iter(), line, -1)

log_handler_console_output(self, line)

Print the line to stdout

Source code in lutris/command.py
def log_handler_console_output(self, line):  # pylint: disable=no-self-use
    """Print the line to stdout"""
    with contextlib.suppress(BlockingIOError):
        sys.stdout.write(line)
        sys.stdout.flush()

log_handler_stdout(self, line)

Add the line to this command's stdout attribute

Source code in lutris/command.py
def log_handler_stdout(self, line):
    """Add the line to this command's stdout attribute"""
    self._stdout.write(line)

on_stdout_output(self, stdout, condition)

Called by the stdout monitor to dispatch output to log handlers

Source code in lutris/command.py
def on_stdout_output(self, stdout, condition):
    """Called by the stdout monitor to dispatch output to log handlers"""
    if condition == GLib.IO_HUP:
        self.stdout_monitor = None
        return False
    if not self.is_running:
        return False
    try:
        line = stdout.read(262144).decode("utf-8", errors="ignore")
    except ValueError:
        # file_desc might be closed
        return True
    if "winemenubuilder.exe" in line:
        return True
    for log_handler in self.log_handlers:
        log_handler(line)
    return True

on_stop(self, pid, _user_data)

Callback registered on game process termination

Source code in lutris/command.py
def on_stop(self, pid, _user_data):
    """Callback registered on game process termination"""
    if self.prevent_on_stop:  # stop() already in progress
        return False
    self.game_process.wait()
    self.return_code = self.get_return_code()
    self.is_running = False
    logger.debug("Process %s has terminated with code %s", pid, self.return_code)
    resume_stop = self.stop()
    if not resume_stop:
        logger.info("Full shutdown prevented")
        return False
    return False

set_log_buffer(self, log_buffer)

Attach a TextBuffer to this command enables the buffer handler

Source code in lutris/command.py
def set_log_buffer(self, log_buffer):
    """Attach a TextBuffer to this command enables the buffer handler"""
    if not log_buffer:
        return
    self.log_buffer = log_buffer
    if self.log_handler_buffer not in self.log_handlers:
        self.log_handlers.append(self.log_handler_buffer)

start(self)

Run the thread.

Source code in lutris/command.py
def start(self):
    """Run the thread."""
    for key, value in self.env.items():
        logger.debug("%s=\"%s\"", key, value)
    wrapper_command = self.get_wrapper_command()
    env = self.get_child_environment()
    self.game_process = self.execute_process(wrapper_command, env)

    if not self.game_process:
        logger.error("No game process available")
        return

    GLib.child_watch_add(self.game_process.pid, self.on_stop)

    # make stdout nonblocking.
    fileno = self.game_process.stdout.fileno()
    fcntl.fcntl(fileno, fcntl.F_SETFL, fcntl.fcntl(fileno, fcntl.F_GETFL) | os.O_NONBLOCK)

    self.stdout_monitor = GLib.io_add_watch(
        self.game_process.stdout,
        GLib.IO_IN | GLib.IO_HUP,
        self.on_stdout_output,
    )

stop(self)

Stops the current game process and cleans up the instance

Source code in lutris/command.py
def stop(self):
    """Stops the current game process and cleans up the instance"""
    # Prevent stop() being called again by the process exiting
    self.prevent_on_stop = True

    try:
        self.game_process.terminate()
    except ProcessLookupError:
        # process already dead.
        pass

    resume_stop = self.stop_func()
    if not resume_stop:
        logger.warning("Stop execution halted by demand of stop_func")
        return False

    if self.stdout_monitor:
        GLib.source_remove(self.stdout_monitor)
        self.stdout_monitor = None

    self.is_running = False
    self.ready_state = False
    return True

exec_command(command)

Execute arbitrary command in a MonitoredCommand

Used by the --exec command line flag.

Source code in lutris/command.py
def exec_command(command):
    """Execute arbitrary command in a MonitoredCommand

    Used by the --exec command line flag.
    """
    command = MonitoredCommand(shlex.split(command), env=runtime.get_env())
    command.start()
    return command

get_wrapper_script_location()

Return absolute path of lutris-wrapper script

Source code in lutris/command.py
def get_wrapper_script_location():
    """Return absolute path of lutris-wrapper script"""
    wrapper_relpath = "share/lutris/bin/lutris-wrapper"
    candidates = [
        os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "..")),
        os.path.dirname(os.path.dirname(settings.__file__)),
        "/usr",
        "/usr/local",
    ]
    for candidate in candidates:
        wrapper_abspath = os.path.join(candidate, wrapper_relpath)
        if os.path.isfile(wrapper_abspath):
            return wrapper_abspath
    raise FileNotFoundError("Couldn't find lutris-wrapper script in any of the expected locations")

config

Handle the game, runner and global system configurations.

LutrisConfig

Class where all the configuration handling happens.

Description

Lutris' configuration uses a cascading mechanism where each higher, more specific level overrides the lower ones

The levels are (highest to lowest): game, runner and system. Each level has its own set of options (config section), available to and overridden by upper levels:

 level | Config sections
-------|----------------------
  game | system, runner, game
runner | system, runner
system | system
Example: if requesting runner options at game level, their returned value will be from the game level config if it's set at this level; if not it will be the value from runner level if available; and if not, the default value set in the runner's module, or None.

The config levels are stored in separate YAML format text files.

Usage

The config level will be auto set depending on what you pass to init: - For game level, pass game_config_id and optionally runner_slug (better perfs) - For runner level, pass runner_slug - For system level, pass nothing If need be, you can pass the level manually.

To read, use the config sections dicts: game_config, runner_config and system_config.

To write, modify the relevant raw_*_config section dict, then run save().

Source code in lutris/config.py
class LutrisConfig:

    """Class where all the configuration handling happens.

    Description
    ===========
    Lutris' configuration uses a cascading mechanism where
    each higher, more specific level overrides the lower ones

    The levels are (highest to lowest): `game`, `runner` and `system`.
    Each level has its own set of options (config section), available to and
    overridden by upper levels:
    ```
     level | Config sections
    -------|----------------------
      game | system, runner, game
    runner | system, runner
    system | system
    ```
    Example: if requesting runner options at game level, their returned value
    will be from the game level config if it's set at this level; if not it
    will be the value from runner level if available; and if not, the default
    value set in the runner's module, or None.

    The config levels are stored in separate YAML format text files.

    Usage
    =====
    The config level will be auto set depending on what you pass to __init__:
    - For game level, pass game_config_id and optionally runner_slug (better perfs)
    - For runner level, pass runner_slug
    - For system level, pass nothing
    If need be, you can pass the level manually.

    To read, use the config sections dicts: game_config, runner_config and
    system_config.

    To write, modify the relevant `raw_*_config` section dict, then run
    `save()`.

    """

    def __init__(self, runner_slug=None, game_config_id=None, level=None):
        self.game_config_id = game_config_id
        if runner_slug:
            self.runner_slug = str(runner_slug)
        else:
            self.runner_slug = runner_slug

        # Cascaded config sections (for reading)
        self.game_config = {}
        self.runner_config = {}
        self.system_config = {}

        # Raw (non-cascaded) sections (for writing)
        self.raw_game_config = {}
        self.raw_runner_config = {}
        self.raw_system_config = {}

        self.raw_config = {}

        # Set config level
        self.level = level
        if not level:
            if game_config_id:
                self.level = "game"
            elif runner_slug:
                self.level = "runner"
            else:
                self.level = "system"
        self.initialize_config()

    def __repr__(self):
        return "LutrisConfig(level=%s, game_config_id=%s, runner=%s)" % (
            self.level,
            self.game_config_id,
            self.runner_slug,
        )

    @property
    def system_config_path(self):
        return os.path.join(settings.CONFIG_DIR, "system.yml")

    @property
    def runner_config_path(self):
        if not self.runner_slug:
            return None
        return os.path.join(settings.CONFIG_DIR, "runners/%s.yml" % self.runner_slug)

    @property
    def game_config_path(self):
        if not self.game_config_id:
            return None
        return os.path.join(settings.CONFIG_DIR, "games/%s.yml" % self.game_config_id)

    def initialize_config(self):
        """Init and load config files"""
        self.game_level = {"system": {}, self.runner_slug: {}, "game": {}}
        self.runner_level = {"system": {}, self.runner_slug: {}}
        self.system_level = {"system": {}}
        self.game_level.update(read_yaml_from_file(self.game_config_path))
        self.runner_level.update(read_yaml_from_file(self.runner_config_path))
        self.system_level.update(read_yaml_from_file(self.system_config_path))

        self.update_cascaded_config()
        self.update_raw_config()

    def update_cascaded_config(self):
        if self.system_level.get("system") is None:
            self.system_level["system"] = {}
        self.system_config.clear()
        self.system_config.update(self.get_defaults("system"))
        self.system_config.update(self.system_level.get("system"))

        if self.level in ["runner", "game"] and self.runner_slug:
            if self.runner_level.get(self.runner_slug) is None:
                self.runner_level[self.runner_slug] = {}
            if self.runner_level.get("system") is None:
                self.runner_level["system"] = {}
            self.runner_config.clear()
            self.runner_config.update(self.get_defaults("runner"))
            self.runner_config.update(self.runner_level.get(self.runner_slug))
            self.merge_to_system_config(self.runner_level.get("system"))

        if self.level == "game" and self.runner_slug:
            if self.game_level.get("game") is None:
                self.game_level["game"] = {}
            if self.game_level.get(self.runner_slug) is None:
                self.game_level[self.runner_slug] = {}
            if self.game_level.get("system") is None:
                self.game_level["system"] = {}
            self.game_config.clear()
            self.game_config.update(self.get_defaults("game"))
            self.game_config.update(self.game_level.get("game"))
            self.runner_config.update(self.game_level.get(self.runner_slug))
            self.merge_to_system_config(self.game_level.get("system"))

    def merge_to_system_config(self, config):
        """Merge a configuration to the system configuation"""
        if not config:
            return
        existing_env = None
        if self.system_config.get("env") and "env" in config:
            existing_env = self.system_config["env"]
        self.system_config.update(config)
        if existing_env:
            self.system_config["env"] = existing_env
            self.system_config["env"].update(config["env"])

    def update_raw_config(self):
        # Select the right level of config
        if self.level == "game":
            raw_config = self.game_level
        elif self.level == "runner":
            raw_config = self.runner_level
        else:
            raw_config = self.system_level

        # Load config sections
        self.raw_system_config = raw_config["system"]
        if self.level in ["runner", "game"]:
            self.raw_runner_config = raw_config[self.runner_slug]
        if self.level == "game":
            self.raw_game_config = raw_config["game"]

        self.raw_config = raw_config

    def remove(self):
        """Delete the configuration file from disk."""
        if not self.game_config_path:
            raise RuntimeError("Tried to remove a non-existent config")
        if not path_exists(self.game_config_path):
            logger.debug("No config file at %s", self.game_config_path)
            return
        os.remove(self.game_config_path)
        logger.debug("Removed config %s", self.game_config_path)

    def save(self):
        """Save configuration file according to its type"""
        if self.level == "system":
            config = self.system_level
            config_path = self.system_config_path
        elif self.level == "runner":
            config = self.runner_level
            config_path = self.runner_config_path
        elif self.level == "game":
            config = self.game_level
            config_path = self.game_config_path
        else:
            raise ValueError("Invalid config level '%s'" % self.level)

        logger.debug("Saving %s config to %s", self, config_path)
        write_yaml_to_file(config, config_path)
        self.initialize_config()

    def get_defaults(self, options_type):
        """Return a dict of options' default value."""
        options_dict = self.options_as_dict(options_type)
        defaults = {}
        for option, params in options_dict.items():
            if "default" in params:
                defaults[option] = params["default"]
        return defaults

    def options_as_dict(self, options_type):
        """Convert the option list to a dict with option name as keys"""
        if options_type == "system":
            options = (
                sysoptions.with_runner_overrides(self.runner_slug) if self.runner_slug else sysoptions.system_options
            )
        else:
            if not self.runner_slug:
                return None
            attribute_name = options_type + "_options"

            try:
                runner = import_runner(self.runner_slug)
            except InvalidRunner:
                options = {}
            else:
                if not getattr(runner, attribute_name):
                    runner = runner()

                options = getattr(runner, attribute_name)
        return dict((opt["option"], opt) for opt in options)

game_config_path property readonly

runner_config_path property readonly

system_config_path property readonly

__init__(self, runner_slug=None, game_config_id=None, level=None) special

Source code in lutris/config.py
def __init__(self, runner_slug=None, game_config_id=None, level=None):
    self.game_config_id = game_config_id
    if runner_slug:
        self.runner_slug = str(runner_slug)
    else:
        self.runner_slug = runner_slug

    # Cascaded config sections (for reading)
    self.game_config = {}
    self.runner_config = {}
    self.system_config = {}

    # Raw (non-cascaded) sections (for writing)
    self.raw_game_config = {}
    self.raw_runner_config = {}
    self.raw_system_config = {}

    self.raw_config = {}

    # Set config level
    self.level = level
    if not level:
        if game_config_id:
            self.level = "game"
        elif runner_slug:
            self.level = "runner"
        else:
            self.level = "system"
    self.initialize_config()

__repr__(self) special

Source code in lutris/config.py
def __repr__(self):
    return "LutrisConfig(level=%s, game_config_id=%s, runner=%s)" % (
        self.level,
        self.game_config_id,
        self.runner_slug,
    )

get_defaults(self, options_type)

Return a dict of options' default value.

Source code in lutris/config.py
def get_defaults(self, options_type):
    """Return a dict of options' default value."""
    options_dict = self.options_as_dict(options_type)
    defaults = {}
    for option, params in options_dict.items():
        if "default" in params:
            defaults[option] = params["default"]
    return defaults

initialize_config(self)

Init and load config files

Source code in lutris/config.py
def initialize_config(self):
    """Init and load config files"""
    self.game_level = {"system": {}, self.runner_slug: {}, "game": {}}
    self.runner_level = {"system": {}, self.runner_slug: {}}
    self.system_level = {"system": {}}
    self.game_level.update(read_yaml_from_file(self.game_config_path))
    self.runner_level.update(read_yaml_from_file(self.runner_config_path))
    self.system_level.update(read_yaml_from_file(self.system_config_path))

    self.update_cascaded_config()
    self.update_raw_config()

merge_to_system_config(self, config)

Merge a configuration to the system configuation

Source code in lutris/config.py
def merge_to_system_config(self, config):
    """Merge a configuration to the system configuation"""
    if not config:
        return
    existing_env = None
    if self.system_config.get("env") and "env" in config:
        existing_env = self.system_config["env"]
    self.system_config.update(config)
    if existing_env:
        self.system_config["env"] = existing_env
        self.system_config["env"].update(config["env"])

options_as_dict(self, options_type)

Convert the option list to a dict with option name as keys

Source code in lutris/config.py
def options_as_dict(self, options_type):
    """Convert the option list to a dict with option name as keys"""
    if options_type == "system":
        options = (
            sysoptions.with_runner_overrides(self.runner_slug) if self.runner_slug else sysoptions.system_options
        )
    else:
        if not self.runner_slug:
            return None
        attribute_name = options_type + "_options"

        try:
            runner = import_runner(self.runner_slug)
        except InvalidRunner:
            options = {}
        else:
            if not getattr(runner, attribute_name):
                runner = runner()

            options = getattr(runner, attribute_name)
    return dict((opt["option"], opt) for opt in options)

remove(self)

Delete the configuration file from disk.

Source code in lutris/config.py
def remove(self):
    """Delete the configuration file from disk."""
    if not self.game_config_path:
        raise RuntimeError("Tried to remove a non-existent config")
    if not path_exists(self.game_config_path):
        logger.debug("No config file at %s", self.game_config_path)
        return
    os.remove(self.game_config_path)
    logger.debug("Removed config %s", self.game_config_path)

save(self)

Save configuration file according to its type

Source code in lutris/config.py
def save(self):
    """Save configuration file according to its type"""
    if self.level == "system":
        config = self.system_level
        config_path = self.system_config_path
    elif self.level == "runner":
        config = self.runner_level
        config_path = self.runner_config_path
    elif self.level == "game":
        config = self.game_level
        config_path = self.game_config_path
    else:
        raise ValueError("Invalid config level '%s'" % self.level)

    logger.debug("Saving %s config to %s", self, config_path)
    write_yaml_to_file(config, config_path)
    self.initialize_config()

update_cascaded_config(self)

Source code in lutris/config.py
def update_cascaded_config(self):
    if self.system_level.get("system") is None:
        self.system_level["system"] = {}
    self.system_config.clear()
    self.system_config.update(self.get_defaults("system"))
    self.system_config.update(self.system_level.get("system"))

    if self.level in ["runner", "game"] and self.runner_slug:
        if self.runner_level.get(self.runner_slug) is None:
            self.runner_level[self.runner_slug] = {}
        if self.runner_level.get("system") is None:
            self.runner_level["system"] = {}
        self.runner_config.clear()
        self.runner_config.update(self.get_defaults("runner"))
        self.runner_config.update(self.runner_level.get(self.runner_slug))
        self.merge_to_system_config(self.runner_level.get("system"))

    if self.level == "game" and self.runner_slug:
        if self.game_level.get("game") is None:
            self.game_level["game"] = {}
        if self.game_level.get(self.runner_slug) is None:
            self.game_level[self.runner_slug] = {}
        if self.game_level.get("system") is None:
            self.game_level["system"] = {}
        self.game_config.clear()
        self.game_config.update(self.get_defaults("game"))
        self.game_config.update(self.game_level.get("game"))
        self.runner_config.update(self.game_level.get(self.runner_slug))
        self.merge_to_system_config(self.game_level.get("system"))

update_raw_config(self)

Source code in lutris/config.py
def update_raw_config(self):
    # Select the right level of config
    if self.level == "game":
        raw_config = self.game_level
    elif self.level == "runner":
        raw_config = self.runner_level
    else:
        raw_config = self.system_level

    # Load config sections
    self.raw_system_config = raw_config["system"]
    if self.level in ["runner", "game"]:
        self.raw_runner_config = raw_config[self.runner_slug]
    if self.level == "game":
        self.raw_game_config = raw_config["game"]

    self.raw_config = raw_config

duplicate_game_config(game_slug, source_config_id)

Copies an existing configuration file, giving it a new id that this function returns.

Source code in lutris/config.py
def duplicate_game_config(game_slug, source_config_id):
    """Copies an existing configuration file, giving it a new id that this
    function returns."""
    new_config_id = make_game_config_id(game_slug)
    src_path = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % source_config_id)
    dest_path = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % new_config_id)
    copyfile(src_path, dest_path)
    return new_config_id

make_game_config_id(game_slug)

Return an unique config id to avoid clashes between multiple games

Source code in lutris/config.py
def make_game_config_id(game_slug):
    """Return an unique config id to avoid clashes between multiple games"""
    return "{}-{}".format(game_slug, int(time.time()))

write_game_config(game_slug, config)

Writes a game config to disk

Source code in lutris/config.py
def write_game_config(game_slug, config):
    """Writes a game config to disk"""
    configpath = make_game_config_id(game_slug)
    logger.debug("Writing game config to %s", configpath)
    config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % configpath)
    write_yaml_to_file(config, config_filename)
    return configpath

database special

categories

add_category(category_name)

Add a category to the database

Source code in lutris/database/categories.py
def add_category(category_name):
    """Add a category to the database"""
    return sql.db_insert(settings.PGA_DB, "categories", {"name": category_name})

add_game_to_category(game_id, category_id)

Add a category to a game

Source code in lutris/database/categories.py
def add_game_to_category(game_id, category_id):
    """Add a category to a game"""
    return sql.db_insert(settings.PGA_DB, "games_categories", {"game_id": game_id, "category_id": category_id})

get_categories()

Get the list of every category in database.

Source code in lutris/database/categories.py
def get_categories():
    """Get the list of every category in database."""
    return sql.db_select(settings.PGA_DB, "categories",)

get_categories_in_game(game_id)

Get the categories of a game in database.

Source code in lutris/database/categories.py
def get_categories_in_game(game_id):
    """Get the categories of a game in database."""
    query = (
        "select categories.name from categories "
        "JOIN games_categories ON categories.id = games_categories.category_id "
        "JOIN games ON games.id = games_categories.game_id "
        "WHERE games.id=?"
    )
    return [
        category["name"]
        for category in sql.db_query(settings.PGA_DB, query, (game_id,))
    ]

get_category(name)

Return a category by name

Source code in lutris/database/categories.py
def get_category(name):
    """Return a category by name"""
    categories = sql.db_select(settings.PGA_DB, "categories", condition=("name", name))
    if categories:
        return categories[0]

get_game_ids_for_category(category_name)

Get the ids of games in database.

Source code in lutris/database/categories.py
def get_game_ids_for_category(category_name):
    """Get the ids of games in database."""
    query = (
        "select game_id from games_categories "
        "JOIN categories ON categories.id = games_categories.category_id "
        "WHERE categories.name=?"
    )
    return [
        game["game_id"]
        for game in sql.db_query(settings.PGA_DB, query, (category_name, ))
    ]

remove_category_from_game(game_id, category_id)

Remove a category from a game

Source code in lutris/database/categories.py
def remove_category_from_game(game_id, category_id):
    """Remove a category from a game"""
    query = "DELETE FROM games_categories WHERE category_id=? AND game_id=?"
    with sql.db_cursor(settings.PGA_DB) as cursor:
        sql.cursor_execute(cursor, query, (category_id, game_id))

games

add_game(**game_data)

Add a game to the PGA database.

Source code in lutris/database/games.py
def add_game(**game_data):
    """Add a game to the PGA database."""
    game_data["installed_at"] = int(time.time())
    if "slug" not in game_data:
        game_data["slug"] = slugify(game_data["name"])
    return sql.db_insert(settings.PGA_DB, "games", game_data)

add_games_bulk(games)

Add a list of games to the PGA database. The dicts must have an identical set of keys.

Parameters:

Name Type Description Default
games list

list of games in dict format

required

Returns:

Type Description
list

List of inserted game ids

Source code in lutris/database/games.py
def add_games_bulk(games):
    """
        Add a list of games to the PGA database.
        The dicts must have an identical set of keys.

        Args:
            games (list): list of games in dict format
        Returns:
            list: List of inserted game ids
    """
    return [sql.db_insert(settings.PGA_DB, "games", game) for game in games]

add_or_update(**params)

Add a game to the PGA or update an existing one

If an 'id' is provided in the parameters then it will try to match it, otherwise it will try matching by slug, creating one when possible.

Source code in lutris/database/games.py
def add_or_update(**params):
    """Add a game to the PGA or update an existing one

    If an 'id' is provided in the parameters then it
    will try to match it, otherwise it will try matching
    by slug, creating one when possible.
    """
    game_id = get_matching_game(params)
    if game_id:
        params["id"] = game_id
        sql.db_update(settings.PGA_DB, "games", params, {"id": game_id})
        return game_id
    return add_game(**params)

delete_game(game_id)

Delete a game from the PGA.

Source code in lutris/database/games.py
def delete_game(game_id):
    """Delete a game from the PGA."""
    sql.db_delete(settings.PGA_DB, "games", "id", game_id)

get_game_by_field(value, field='slug')

Query a game based on a database field

Source code in lutris/database/games.py
def get_game_by_field(value, field="slug"):
    """Query a game based on a database field"""
    if field not in ("slug", "installer_slug", "id", "configpath", "name"):
        raise ValueError("Can't query by field '%s'" % field)
    game_result = sql.db_select(settings.PGA_DB, "games", condition=(field, value))
    if game_result:
        return game_result[0]
    return {}

get_game_for_service(service, appid)

Source code in lutris/database/games.py
def get_game_for_service(service, appid):
    existing_games = get_games(filters={"service_id": appid, "service": service})
    if existing_games:
        return existing_games[0]

get_games(searches=None, filters=None, excludes=None, sorts=None)

Source code in lutris/database/games.py
def get_games(
    searches=None,
    filters=None,
    excludes=None,
    sorts=None
):
    return sql.filtered_query(
        settings.PGA_DB,
        "games",
        searches=searches,
        filters=filters,
        excludes=excludes,
        sorts=sorts
    )

get_games_by_ids(game_ids)

Source code in lutris/database/games.py
def get_games_by_ids(game_ids):
    # sqlite limits the number of query parameters to 999, to
    # bypass that limitation, divide the query in chunks
    size = 999
    return list(
        chain.from_iterable(
            [
                get_games_where(id__in=list(game_ids)[page * size:page * size + size])
                for page in range(math.ceil(len(game_ids) / size))
            ]
        )
    )

get_games_by_runner(runner)

Return all games using a specific runner

Source code in lutris/database/games.py
def get_games_by_runner(runner):
    """Return all games using a specific runner"""
    return sql.db_select(settings.PGA_DB, "games", condition=("runner", runner))

get_games_by_slug(slug)

Return all games using a specific slug

Source code in lutris/database/games.py
def get_games_by_slug(slug):
    """Return all games using a specific slug"""
    return sql.db_select(settings.PGA_DB, "games", condition=("slug", slug))

get_games_where(**conditions)

Query games table based on conditions

Parameters:

Name Type Description Default
conditions dict

named arguments with each field matches its desired value.

{}
Special values for field names can be used

__isnull will return rows where field is NULL if the value is True __not will invert the condition using != instead of = __in will match rows for every value of value, which should be an iterable

required

Returns:

Type Description
list

Rows matching the query

Source code in lutris/database/games.py
def get_games_where(**conditions):
    """
        Query games table based on conditions

        Args:
            conditions (dict): named arguments with each field matches its desired value.
            Special values for field names can be used:
                <field>__isnull will return rows where `field` is NULL if the value is True
                <field>__not will invert the condition using `!=` instead of `=`
                <field>__in will match rows for every value of `value`, which should be an iterable

        Returns:
            list: Rows matching the query

    """
    query = "select * from games"
    condition_fields = []
    condition_values = []
    for field, value in conditions.items():
        field, *extra_conditions = field.split("__")
        if extra_conditions:
            extra_condition = extra_conditions[0]
            if extra_condition == "isnull":
                condition_fields.append("{} is {} null".format(field, "" if value else "not"))
            if extra_condition == "not":
                condition_fields.append("{} != ?".format(field))
                condition_values.append(value)
            if extra_condition == "in":
                if not hasattr(value, "__iter__"):
                    raise ValueError("Value should be an iterable (%s given)" % value)
                if len(value) > 999:
                    raise ValueError("SQLite limnited to a maximum of 999 parameters.")
                if value:
                    condition_fields.append("{} in ({})".format(field, ", ".join("?" * len(value)) or ""))
                    condition_values = list(chain(condition_values, value))
        else:
            condition_fields.append("{} = ?".format(field))
            condition_values.append(value)
    condition = " AND ".join(condition_fields)
    if condition:
        query = " WHERE ".join((query, condition))
    else:
        # Inspect and document why we should return
        # an empty list when no condition is present.
        return []
    return sql.db_query(settings.PGA_DB, query, tuple(condition_values))

get_matching_game(params)

Tries to match given parameters with an existing game

Source code in lutris/database/games.py
def get_matching_game(params):
    """Tries to match given parameters with an existing game"""
    # Always match by ID if provided
    if params.get("id"):
        game = get_game_by_field(params["id"], "id")
        if game:
            return game["id"]
        logger.warning("Game ID %s provided but couldn't be matched", params["id"])
    slug = params.get("slug") or slugify(params.get("name"))
    if not slug:
        raise ValueError("Can't add or update without an identifier")
    for game in get_games_by_slug(slug):
        if game["installed"]:
            if game["configpath"] == params.get("configpath"):
                return game["id"]
        else:
            if (game["runner"] == params.get("runner") or not all([params.get("runner"), game["runner"]])):
                return game["id"]
    return None

get_service_games(service)

Return the list of all installed games for a service

Source code in lutris/database/games.py
def get_service_games(service):
    """Return the list of all installed games for a service"""
    global _SERVICE_CACHE_ACCESSED
    previous_cache_accessed = _SERVICE_CACHE_ACCESSED or 0
    _SERVICE_CACHE_ACCESSED = time.time()
    if service not in _SERVICE_CACHE or _SERVICE_CACHE_ACCESSED - previous_cache_accessed > 1:
        if service == "lutris":
            _SERVICE_CACHE[service] = [game["slug"] for game in get_games(filters={"installed": "1"})]
        else:
            _SERVICE_CACHE[service] = [
                game["service_id"] for game in get_games(filters={"service": service, "installed": "1"})
            ]
    return _SERVICE_CACHE[service]

get_unusued_game_name(game_name)

Returns the given name, but if this name is already used by an installed game, this adds a number to it to make it unique.

Source code in lutris/database/games.py
def get_unusued_game_name(game_name):
    """Returns the given name, but if this name is already used by an installed
    game, this adds a number to it to make it unique."""
    def is_name_in_use(name):
        """Queries the database to see if a given is in use by an installed
        game."""
        existing_game = get_game_by_field(assigned_name, "name")
        return existing_game and existing_game["installed"]

    assigned_name = game_name
    assigned_index = 1
    while is_name_in_use(assigned_name):
        assigned_index += 1
        assigned_name = f"{game_name} {assigned_index}"

    return assigned_name

get_used_platforms()

Return a list of platforms currently in use

Source code in lutris/database/games.py
def get_used_platforms():
    """Return a list of platforms currently in use"""
    with sql.db_cursor(settings.PGA_DB) as cursor:
        query = (
            "select distinct platform from games "
            "where platform is not null and platform is not '' order by platform"
        )
        rows = cursor.execute(query)
        results = rows.fetchall()
    return [result[0] for result in results if result[0]]

get_used_runners()

Return a list of the runners in use by installed games.

Source code in lutris/database/games.py
def get_used_runners():
    """Return a list of the runners in use by installed games."""
    with sql.db_cursor(settings.PGA_DB) as cursor:
        query = "select distinct runner from games where runner is not null order by runner"
        rows = cursor.execute(query)
        results = rows.fetchall()
    return [result[0] for result in results if result[0]]

schema

DATABASE

create_table(name, schema)

Creates a new table in the database

Source code in lutris/database/schema.py
def create_table(name, schema):
    """Creates a new table in the database"""
    fields = ", ".join([field_to_string(**f) for f in schema])
    query = "CREATE TABLE IF NOT EXISTS %s (%s)" % (name, fields)
    logger.debug("[PGAQuery] %s", query)
    with sql.db_cursor(settings.PGA_DB) as cursor:
        cursor.execute(query)

field_to_string(name='', type='', indexed=False, unique=False)

Converts a python based table definition to it's SQL statement

Source code in lutris/database/schema.py
def field_to_string(name="", type="", indexed=False, unique=False):  # pylint: disable=redefined-builtin
    """Converts a python based table definition to it's SQL statement"""
    field_query = "%s %s" % (name, type)
    if indexed:
        field_query += " PRIMARY KEY"
    if unique:
        field_query += " UNIQUE"
    return field_query

get_schema(tablename)

Fields

  • position
  • name
  • type
  • not null
  • default
  • indexed
Source code in lutris/database/schema.py
def get_schema(tablename):
    """
    Fields:
        - position
        - name
        - type
        - not null
        - default
        - indexed
    """
    tables = []
    query = "pragma table_info('%s')" % tablename
    with sql.db_cursor(settings.PGA_DB) as cursor:
        for row in cursor.execute(query).fetchall():
            field = {
                "name": row[1],
                "type": row[2],
                "not_null": row[3],
                "default": row[4],
                "indexed": row[5],
            }
            tables.append(field)
    return tables

migrate(table, schema)

Compare a database table with the reference model and make necessary changes

This is very basic and only the needed features have been implemented (adding columns)

Parameters:

Name Type Description Default
table str

Name of the table to migrate

required
schema dict

Reference schema for the table

required

Returns:

Type Description
list

The list of column names that have been added

Source code in lutris/database/schema.py
def migrate(table, schema):
    """Compare a database table with the reference model and make necessary changes

    This is very basic and only the needed features have been implemented (adding columns)

    Args:
        table (str): Name of the table to migrate
        schema (dict): Reference schema for the table

    Returns:
        list: The list of column names that have been added
    """

    existing_schema = get_schema(table)
    migrated_fields = []
    if existing_schema:
        columns = [col["name"] for col in existing_schema]
        for field in schema:
            if field["name"] not in columns:
                logger.info("Migrating %s field %s", table, field["name"])
                migrated_fields.append(field["name"])
                sql.add_field(settings.PGA_DB, table, field)
    else:
        create_table(table, schema)
    return migrated_fields

syncdb()

Update the database to the current version, making necessary changes for backwards compatibility.

Source code in lutris/database/schema.py
def syncdb():
    """Update the database to the current version, making necessary changes
    for backwards compatibility."""
    for table_name, table_data in DATABASE.items():
        migrate(table_name, table_data)

services

ServiceGameCollection

Source code in lutris/database/services.py
class ServiceGameCollection:

    @classmethod
    def get_for_service(cls, service):
        if not service:
            raise ValueError("No service provided")
        return sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service})

    @classmethod
    def get_game(cls, service, appid):
        """Return a single game refered by its appid"""
        if not service:
            raise ValueError("No service provided")
        if not appid:
            raise ValueError("No appid provided")
        results = sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service, "appid": appid})
        if not results:
            return
        if len(results) > 1:
            logger.warning("More than one game found for %s on %s", appid, service)
        return results[0]
get_for_service(service) classmethod
Source code in lutris/database/services.py
@classmethod
def get_for_service(cls, service):
    if not service:
        raise ValueError("No service provided")
    return sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service})
get_game(service, appid) classmethod

Return a single game refered by its appid

Source code in lutris/database/services.py
@classmethod
def get_game(cls, service, appid):
    """Return a single game refered by its appid"""
    if not service:
        raise ValueError("No service provided")
    if not appid:
        raise ValueError("No appid provided")
    results = sql.filtered_query(settings.PGA_DB, "service_games", filters={"service": service, "appid": appid})
    if not results:
        return
    if len(results) > 1:
        logger.warning("More than one game found for %s on %s", appid, service)
    return results[0]

sources

add_source(uri)

Source code in lutris/database/sources.py
def add_source(uri):
    sql.db_insert(settings.PGA_DB, "sources", {"uri": uri})

check_for_file(game, file_id)

Source code in lutris/database/sources.py
def check_for_file(game, file_id):
    for source in read_sources():
        if source.startswith("file://"):
            source = source[7:]
        else:
            protocol = source[:7]
            logger.warning("PGA source protocol %s not implemented", protocol)
            continue
        if not system.path_exists(source):
            logger.info("PGA source %s unavailable", source)
            continue
        game_dir = os.path.join(source, game)
        if not system.path_exists(game_dir):
            continue
        for game_file in os.listdir(game_dir):
            game_base, _ext = os.path.splitext(game_file)
            if game_base == file_id:
                return os.path.join(game_dir, game_file)
    return False

delete_source(uri)

Source code in lutris/database/sources.py
def delete_source(uri):
    sql.db_delete(settings.PGA_DB, "sources", "uri", uri)

read_sources()

Source code in lutris/database/sources.py
def read_sources():
    with sql.db_cursor(settings.PGA_DB) as cursor:
        rows = cursor.execute("select uri from sources")
        results = rows.fetchall()
    return [row[0] for row in results]

write_sources(sources)

Source code in lutris/database/sources.py
def write_sources(sources):
    db_sources = read_sources()
    for uri in db_sources:
        if uri not in sources:
            sql.db_delete(settings.PGA_DB, "sources", "uri", uri)
    for uri in sources:
        if uri not in db_sources:
            sql.db_insert(settings.PGA_DB, "sources", {"uri": uri})

sql

DB_LOCK

db_cursor

Source code in lutris/database/sql.py
class db_cursor(object):

    def __init__(self, db_path):
        self.db_path = db_path
        self.db_conn = None

    def __enter__(self):
        self.db_conn = sqlite3.connect(self.db_path)
        cursor = self.db_conn.cursor()
        return cursor

    def __exit__(self, _type, value, traceback):
        self.db_conn.commit()
        self.db_conn.close()
__enter__(self) special
Source code in lutris/database/sql.py
def __enter__(self):
    self.db_conn = sqlite3.connect(self.db_path)
    cursor = self.db_conn.cursor()
    return cursor
__exit__(self, _type, value, traceback) special
Source code in lutris/database/sql.py
def __exit__(self, _type, value, traceback):
    self.db_conn.commit()
    self.db_conn.close()
__init__(self, db_path) special
Source code in lutris/database/sql.py
def __init__(self, db_path):
    self.db_path = db_path
    self.db_conn = None

add_field(db_path, tablename, field)

Source code in lutris/database/sql.py
def add_field(db_path, tablename, field):
    query = "ALTER TABLE %s ADD COLUMN %s %s" % (
        tablename,
        field["name"],
        field["type"],
    )
    with db_cursor(db_path) as cursor:
        cursor.execute(query)

cursor_execute(cursor, query, params=None)

Execute a SQL query, run it in a lock block

Source code in lutris/database/sql.py
def cursor_execute(cursor, query, params=None):
    """Execute a SQL query, run it in a lock block"""
    params = params or ()
    lock = DB_LOCK.acquire(timeout=1)  # pylint: disable=consider-using-with
    if not lock:
        logger.error("Database is busy. Not executing %s", query)
        return
    results = cursor.execute(query, params)
    DB_LOCK.release()
    return results

db_delete(db_path, table, field, value)

Source code in lutris/database/sql.py
def db_delete(db_path, table, field, value):
    with db_cursor(db_path) as cursor:
        cursor_execute(cursor, "delete from {0} where {1}=?".format(table, field), (value, ))

db_insert(db_path, table, fields)

Source code in lutris/database/sql.py
def db_insert(db_path, table, fields):
    columns = ", ".join(list(fields.keys()))
    placeholders = ("?, " * len(fields))[:-2]
    field_values = tuple(fields.values())
    with db_cursor(db_path) as cursor:
        cursor_execute(
            cursor,
            "insert into {0}({1}) values ({2})".format(table, columns, placeholders),
            field_values,
        )
        inserted_id = cursor.lastrowid
    return inserted_id

db_query(db_path, query, params=())

Source code in lutris/database/sql.py
def db_query(db_path, query, params=()):
    with db_cursor(db_path) as cursor:
        cursor_execute(cursor, query, params)
        rows = cursor.fetchall()
        column_names = [column[0] for column in cursor.description]
    results = []
    for row in rows:
        row_data = {}
        for index, column in enumerate(column_names):
            row_data[column] = row[index]
        results.append(row_data)
    return results

db_select(db_path, table, fields=None, condition=None)

Source code in lutris/database/sql.py
def db_select(db_path, table, fields=None, condition=None):
    if fields:
        columns = ", ".join(fields)
    else:
        columns = "*"
    with db_cursor(db_path) as cursor:
        query = "SELECT {} FROM {}"
        if condition:
            condition_field, condition_value = condition
            if isinstance(condition_value, (list, tuple, set)):
                condition_value = tuple(condition_value)
                placeholders = ", ".join("?" * len(condition_value))
                where_condition = " where {} in (" + placeholders + ")"
            else:
                condition_value = (condition_value, )
                where_condition = " where {}=?"
            query = query + where_condition
            query = query.format(columns, table, condition_field)
            params = condition_value
        else:
            query = query.format(columns, table)
            params = ()
        cursor_execute(cursor, query, params)
        rows = cursor.fetchall()
        column_names = [column[0] for column in cursor.description]
    results = []
    for row in rows:
        row_data = {}
        for index, column in enumerate(column_names):
            row_data[column] = row[index]
        results.append(row_data)
    return results

db_update(db_path, table, updated_fields, conditions)

Update table with the values given in the dict values on the condition given with the row tuple.

Source code in lutris/database/sql.py
def db_update(db_path, table, updated_fields, conditions):
    """Update `table` with the values given in the dict `values` on the
       condition given with the `row` tuple.
    """
    columns = "=?, ".join(list(updated_fields.keys())) + "=?"
    field_values = tuple(updated_fields.values())

    condition_field = " AND ".join(["%s=?" % field for field in conditions])
    condition_value = tuple(conditions.values())

    with db_cursor(db_path) as cursor:
        query = "UPDATE {0} SET {1} WHERE {2}".format(table, columns, condition_field)
        result = cursor_execute(cursor, query, field_values + condition_value)
    return result

filtered_query(db_path, table, searches=None, filters=None, excludes=None, sorts=None)

Source code in lutris/database/sql.py
def filtered_query(
    db_path,
    table,
    searches=None,
    filters=None,
    excludes=None,
    sorts=None
):
    query = "select * from %s" % table
    params = []
    sql_filters = []
    for field in searches or {}:
        sql_filters.append("%s LIKE ?" % field)
        params.append("%" + searches[field] + "%")
    for field in filters or {}:
        if filters[field] is not None:  # but 0 or False are okay!
            sql_filters.append("%s = ?" % field)
            params.append(filters[field])
    for field in excludes or {}:
        if excludes[field]:
            sql_filters.append("%s IS NOT ?" % field)
            params.append(excludes[field])
    if sql_filters:
        query += " WHERE " + " AND ".join(sql_filters)
    if sorts:
        query += " ORDER BY %s" % ", ".join(
            ["%s %s" % (sort[0], sort[1]) for sort in sorts]
        )
    else:
        query += " ORDER BY slug ASC"
    return db_query(db_path, query, tuple(params))

discord

Discord integration

PyPresence

PyPresenceException

DiscordPresence

Provide rich presence integration with Discord for games

Source code in lutris/discord.py
class DiscordPresence(object):

    """Provide rich presence integration with Discord for games"""

    def __init__(self):
        self.available = bool(PyPresence)
        self.game_name = ""
        self.runner_name = ""
        self.last_rpc = 0
        self.rpc_interval = 60
        self.presence_connected = False
        self.rpc_client = None
        self.client_id = None

    def connect(self):
        """Make sure we are actually connected before trying to send requests"""
        if not self.presence_connected:
            self.rpc_client = PyPresence(self.client_id)
            try:
                self.rpc_client.connect()
                self.presence_connected = True
            except (ConnectionError, FileNotFoundError):
                logger.error("Could not connect to Discord")
        return self.presence_connected

    def disconnect(self):
        """Ensure we are definitely disconnected and fix broken event loop from pypresence
        That method is a huge mess of non-deterministic bs and should be nuked from orbit.
        """
        if self.rpc_client:
            try:
                self.rpc_client.close()
            except Exception as e:
                logger.exception("Unable to close Discord RPC connection: %s", e)
            if self.rpc_client.sock_writer is not None:
                try:
                    self.rpc_client.sock_writer.close()
                except Exception:
                    logger.exception("Sock writer could not be closed.")
            try:
                logger.debug("Forcefully closing event loop.")
                self.rpc_client.loop.close()
            except Exception:
                logger.debug("Could not close event loop.")
            try:
                logger.debug("Forcefully replacing event loop.")
                self.rpc_client.loop = None
                asyncio.set_event_loop(asyncio.new_event_loop())
            except Exception as e:
                logger.exception("Could not replace event loop: %s", e)
            try:
                logger.debug("Forcefully deleting RPC client.")
                self.rpc_client = None
            except Exception as ex:
                logger.exception(ex)
        self.rpc_client = None
        self.presence_connected = False

    def update_discord_rich_presence(self):
        """Dispatch a request to Discord to update presence"""
        if int(time.time()) - self.rpc_interval < self.last_rpc:
            logger.debug("Not enough time since last RPC")
            return

        self.last_rpc = int(time.time())
        if not self.connect():
            return
        try:
            self.rpc_client.update(details="Playing %s" % self.game_name,
                                   large_image="large_image",
                                   large_text=self.game_name,
                                   small_image="small_image")
        except PyPresenceException as ex:
            logger.error("Unable to update Discord: %s", ex)

    def clear_discord_rich_presence(self):
        """Dispatch a request to Discord to clear presence"""
        if self.connect():
            try:
                self.rpc_client.clear()
            except PyPresenceException as ex:
                logger.error("Unable to clear Discord: %s", ex)
                self.disconnect()

__init__(self) special

Source code in lutris/discord.py
def __init__(self):
    self.available = bool(PyPresence)
    self.game_name = ""
    self.runner_name = ""
    self.last_rpc = 0
    self.rpc_interval = 60
    self.presence_connected = False
    self.rpc_client = None
    self.client_id = None

clear_discord_rich_presence(self)

Dispatch a request to Discord to clear presence

Source code in lutris/discord.py
def clear_discord_rich_presence(self):
    """Dispatch a request to Discord to clear presence"""
    if self.connect():
        try:
            self.rpc_client.clear()
        except PyPresenceException as ex:
            logger.error("Unable to clear Discord: %s", ex)
            self.disconnect()

connect(self)

Make sure we are actually connected before trying to send requests

Source code in lutris/discord.py
def connect(self):
    """Make sure we are actually connected before trying to send requests"""
    if not self.presence_connected:
        self.rpc_client = PyPresence(self.client_id)
        try:
            self.rpc_client.connect()
            self.presence_connected = True
        except (ConnectionError, FileNotFoundError):
            logger.error("Could not connect to Discord")
    return self.presence_connected

disconnect(self)

Ensure we are definitely disconnected and fix broken event loop from pypresence That method is a huge mess of non-deterministic bs and should be nuked from orbit.

Source code in lutris/discord.py
def disconnect(self):
    """Ensure we are definitely disconnected and fix broken event loop from pypresence
    That method is a huge mess of non-deterministic bs and should be nuked from orbit.
    """
    if self.rpc_client:
        try:
            self.rpc_client.close()
        except Exception as e:
            logger.exception("Unable to close Discord RPC connection: %s", e)
        if self.rpc_client.sock_writer is not None:
            try:
                self.rpc_client.sock_writer.close()
            except Exception:
                logger.exception("Sock writer could not be closed.")
        try:
            logger.debug("Forcefully closing event loop.")
            self.rpc_client.loop.close()
        except Exception:
            logger.debug("Could not close event loop.")
        try:
            logger.debug("Forcefully replacing event loop.")
            self.rpc_client.loop = None
            asyncio.set_event_loop(asyncio.new_event_loop())
        except Exception as e:
            logger.exception("Could not replace event loop: %s", e)
        try:
            logger.debug("Forcefully deleting RPC client.")
            self.rpc_client = None
        except Exception as ex:
            logger.exception(ex)
    self.rpc_client = None
    self.presence_connected = False

update_discord_rich_presence(self)

Dispatch a request to Discord to update presence

Source code in lutris/discord.py
def update_discord_rich_presence(self):
    """Dispatch a request to Discord to update presence"""
    if int(time.time()) - self.rpc_interval < self.last_rpc:
        logger.debug("Not enough time since last RPC")
        return

    self.last_rpc = int(time.time())
    if not self.connect():
        return
    try:
        self.rpc_client.update(details="Playing %s" % self.game_name,
                               large_image="large_image",
                               large_text=self.game_name,
                               small_image="small_image")
    except PyPresenceException as ex:
        logger.error("Unable to update Discord: %s", ex)

exceptions

Exception handling module

AuthenticationError (Exception)

Raised when authentication to a service fails

Source code in lutris/exceptions.py
class AuthenticationError(Exception):
    """Raised when authentication to a service fails"""

GameConfigError (LutrisError)

Throw this error when the game configuration prevents the game from running properly.

Source code in lutris/exceptions.py
class GameConfigError(LutrisError):

    """Throw this error when the game configuration prevents the game from
    running properly.
    """

LutrisError (Exception)

Base exception for Lutris related errors

Source code in lutris/exceptions.py
class LutrisError(Exception):

    """Base exception for Lutris related errors"""

    def __init__(self, message):
        super().__init__(message)
        self.message = message

__init__(self, message) special

Source code in lutris/exceptions.py
def __init__(self, message):
    super().__init__(message)
    self.message = message

MultipleInstallerError (BaseException)

Current implementation doesn't know how to deal with multiple installers Raise this if a game returns more than 1 installer.

Source code in lutris/exceptions.py
class MultipleInstallerError(BaseException):

    """Current implementation doesn't know how to deal with multiple installers
    Raise this if a game returns more than 1 installer."""

UnavailableGame (Exception)

Raised when a game is available from a service

Source code in lutris/exceptions.py
class UnavailableGame(Exception):
    """Raised when a game is available from a service"""

UnavailableLibraries (RuntimeError)

Source code in lutris/exceptions.py
class UnavailableLibraries(RuntimeError):

    def __init__(self, libraries, arch=None):
        message = _(
            "The following {arch} libraries are required but are not installed on your system:\n{libs}"
        ).format(
            arch=arch if arch else "",
            libs=", ".join(libraries)
        )
        super().__init__(message)
        self.libraries = libraries

__init__(self, libraries, arch=None) special

Source code in lutris/exceptions.py
def __init__(self, libraries, arch=None):
    message = _(
        "The following {arch} libraries are required but are not installed on your system:\n{libs}"
    ).format(
        arch=arch if arch else "",
        libs=", ".join(libraries)
    )
    super().__init__(message)
    self.libraries = libraries

watch_lutris_errors(function)

Decorator used to catch LutrisError exceptions and send events

Source code in lutris/exceptions.py
def watch_lutris_errors(function):
    """Decorator used to catch LutrisError exceptions and send events"""

    @wraps(function)
    def wrapper(*args, **kwargs):
        """Catch all LutrisError exceptions and emit an event."""
        try:
            return function(*args, **kwargs)
        except LutrisError as ex:
            game = args[0]
            game.emit("game-error", ex.message)

    return wrapper

game

Module that actually runs the games.

HEARTBEAT_DELAY

Game (Object)

This class takes cares of loading the configuration for a game and running it.

Source code in lutris/game.py
class Game(GObject.Object):
    """This class takes cares of loading the configuration for a game
       and running it.
    """

    now_playing_path = os.path.join(settings.CACHE_DIR, "now-playing.txt")

    STATE_STOPPED = "stopped"
    STATE_LAUNCHING = "launching"
    STATE_RUNNING = "running"

    __gsignals__ = {
        "game-error": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
        "game-launch": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-start": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-started": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-stop": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-stopped": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-removed": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-updated": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-install": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-install-update": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-install-dlc": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "game-installed": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, game_id=None):
        super().__init__()
        self.id = game_id  # pylint: disable=invalid-name
        self.runner = None
        self.config = None

        # Load attributes from database
        game_data = games_db.get_game_by_field(game_id, "id")

        self.slug = game_data.get("slug") or ""
        self.runner_name = game_data.get("runner") or ""
        self.directory = game_data.get("directory") or ""
        self.name = game_data.get("name") or ""
        self.game_config_id = game_data.get("configpath") or ""
        self.is_installed = bool(game_data.get("installed") and self.game_config_id)
        self.is_hidden = bool(game_data.get("hidden"))
        self.platform = game_data.get("platform") or ""
        self.year = game_data.get("year") or ""
        self.lastplayed = game_data.get("lastplayed") or 0
        self.has_custom_banner = bool(game_data.get("has_custom_banner"))
        self.has_custom_icon = bool(game_data.get("has_custom_icon"))
        self.service = game_data.get("service")
        self.appid = game_data.get("service_id")
        self.playtime = game_data.get("playtime") or 0.0

        if self.game_config_id:
            self.load_config()
        self.game_uuid = None
        self.game_thread = None
        self.antimicro_thread = None
        self.prelaunch_pids = []
        self.prelaunch_executor = None
        self.heartbeat = None
        self.killswitch = None
        self.state = self.STATE_STOPPED
        self.game_runtime_config = {}
        self.resolution_changed = False
        self.compositor_disabled = False
        self.original_outputs = None
        self._log_buffer = None
        self.timer = Timer()
        self.screen_saver_inhibitor_cookie = None

    def __repr__(self):
        return self.__str__()

    def __str__(self):
        value = self.name or "Game (no name)"
        if self.runner_name:
            value += " (%s)" % self.runner_name
        return value

    @property
    def is_updatable(self):
        """Return whether the game can be upgraded"""
        return self.service == "gog"

    @property
    def is_favorite(self):
        """Return whether the game is in the user's favorites"""
        categories = categories_db.get_categories_in_game(self.id)
        for category in categories:
            if category == "favorite":
                return True
        return False

    def add_to_favorites(self):
        """Add the game to the 'favorite' category"""
        favorite = categories_db.get_category("favorite")
        if not favorite:
            favorite = categories_db.add_category("favorite")
        categories_db.add_game_to_category(self.id, favorite["id"])
        self.emit("game-updated")

    def remove_from_favorites(self):
        """Remove game from favorites"""
        favorite = categories_db.get_category("favorite")
        categories_db.remove_category_from_game(self.id, favorite["id"])
        self.emit("game-updated")

    def set_hidden(self, is_hidden):
        """Do not show this game in the UI"""
        self.is_hidden = is_hidden
        self.save()
        self.emit("game-updated")

    @property
    def log_buffer(self):
        """Access the log buffer object, creating it if necessary"""
        _log_buffer = LOG_BUFFERS.get(str(self.id))
        if _log_buffer:
            return _log_buffer
        _log_buffer = Gtk.TextBuffer()
        _log_buffer.create_tag("warning", foreground="red")
        if self.game_thread:
            self.game_thread.set_log_buffer(self._log_buffer)
            _log_buffer.set_text(self.game_thread.stdout)
        LOG_BUFFERS[str(self.id)] = _log_buffer
        return _log_buffer

    @property
    def formatted_playtime(self):
        """Return a human readable formatted play time"""
        return strings.get_formatted_playtime(self.playtime)

    @staticmethod
    def show_error_message(message):
        """Display an error message based on the runner's output."""
        if message["error"] == "CUSTOM":
            message_text = message["text"].replace("&", "&amp;")
            dialogs.ErrorDialog(message_text)
        elif message["error"] == "RUNNER_NOT_INSTALLED":
            dialogs.ErrorDialog(_("Error the runner is not installed"))
        elif message["error"] == "NO_BIOS":
            dialogs.ErrorDialog(_("A bios file is required to run this game"))
        elif message["error"] == "FILE_NOT_FOUND":
            filename = message["file"]
            if filename:
                message_text = _("The file {} could not be found").format(filename.replace("&", "&amp;"))
            else:
                message_text = _("This game has no executable set. The install process didn't finish properly.")
            dialogs.ErrorDialog(message_text)
        elif message["error"] == "NOT_EXECUTABLE":
            message_text = message["file"].replace("&", "&amp;")
            dialogs.ErrorDialog(_("The file %s is not executable") % message_text)
        elif message["error"] == "PATH_NOT_SET":
            message_text = _("The path '%s' is not set. please set it in the options.") % message["path"]
            dialogs.ErrorDialog(message_text)
        else:
            dialogs.ErrorDialog(_("Unhandled error: %s") % message["error"])

    def get_browse_dir(self):
        """Return the path to open with the Browse Files action."""
        return self.runner.game_path

    def _get_runner(self):
        """Return the runner instance for this game's configuration"""
        try:
            runner_class = import_runner(self.runner_name)
            return runner_class(self.config)
        except InvalidRunner:
            logger.error("Unable to import runner %s for %s", self.runner_name, self.slug)

    def load_config(self):
        """Load the game's configuration."""
        if not self.is_installed:
            return
        self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id)
        self.runner = self._get_runner()

    def set_desktop_compositing(self, enable):
        """Enables or disables compositing"""
        if enable:
            if self.compositor_disabled:
                enable_compositing()
                self.compositor_disabled = False
        else:
            if not self.compositor_disabled:
                disable_compositing()
                self.compositor_disabled = True

    def remove(self, delete_files=False, no_signal=False):
        """Uninstall a game

        Params:
            delete_files (bool): Delete the game files
            no_signal (bool): Don't emit game-removed signal (if running in a thread)
        """
        sql.db_update(settings.PGA_DB, "games", {"installed": 0, "runner": ""}, {"id": self.id})
        if self.config:
            self.config.remove()
        xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True)
        if delete_files and self.runner:
            self.runner.remove_game_data(game_path=self.directory)
        self.is_installed = False
        self.runner = None
        if no_signal:
            return
        self.emit("game-removed")

    def delete(self):
        """Completely remove a game from the library"""
        if self.is_installed:
            raise RuntimeError("Uninstall the game before deleting")
        games_db.delete_game(self.id)
        self.emit("game-removed")

    def set_platform_from_runner(self):
        """Set the game's platform from the runner"""
        if not self.runner:
            logger.warning("Game has no runner, can't set platform")
            return
        self.platform = self.runner.get_platform()
        if not self.platform:
            logger.warning("The %s runner didn't provide a platform for %s", self.runner.human_name, self)

    def save(self, save_config=False):
        """
        Save the game's config and metadata, if `save_config` is set to False,
        do not save the config. This is useful when exiting the game since the
        config might have changed and we don't want to override the changes.
        """
        if self.config:
            logger.debug("Saving %s with config ID %s", self, self.config.game_config_id)
            configpath = self.config.game_config_id
            if save_config:
                self.config.save()
        else:
            logger.warning("Saving %s without a configuration", self)
            configpath = ""
        self.set_platform_from_runner()
        self.id = games_db.add_or_update(
            name=self.name,
            runner=self.runner_name,
            slug=self.slug,
            platform=self.platform,
            directory=self.directory,
            installed=self.is_installed,
            year=self.year,
            lastplayed=self.lastplayed,
            configpath=configpath,
            id=self.id,
            playtime=self.playtime,
            hidden=self.is_hidden,
            service=self.service,
            service_id=self.appid,
        )
        self.emit("game-updated")

    def is_launchable(self):
        """Verify that the current game can be launched."""
        if not self.is_installed:
            logger.error("%s (%s) not installed", self, self.id)
            dialogs.ErrorDialog(_("Tried to launch a game that isn't installed."))
            return False
        if not self.runner:
            dialogs.ErrorDialog(_("Invalid game configuration: Missing runner"))
            return False
        if not self.runner.is_installed():
            installed = self.runner.install_dialog()
            if not installed:
                dialogs.ErrorDialog(_("Runner not installed."))
                return False

        if self.runner.use_runtime():
            runtime_updater = runtime.RuntimeUpdater()
            if runtime_updater.is_updating():
                dialogs.ErrorDialog(_("Runtime currently updating"), _("Game might not work as expected"))
        if ("wine" in self.runner_name and not wine.get_wine_version() and not LINUX_SYSTEM.is_flatpak):
            dialogs.WineNotInstalledWarning(parent=None)
        return True

    def restrict_to_display(self, display):
        outputs = DISPLAY_MANAGER.get_config()
        if display == "primary":
            display = None
            for output in outputs:
                if output.primary:
                    display = output.name
                    break
            if not display:
                logger.warning("No primary display set")
        else:
            found = False
            for output in outputs:
                if output.name == display:
                    found = True
                    break
            if not found:
                logger.warning("Selected display %s not found", display)
                display = None
        if display:
            turn_off_except(display)
            time.sleep(3)
            return True
        return False

    def start_xephyr(self, display=":2"):
        """Start a monitored Xephyr instance"""
        if not system.find_executable("Xephyr"):
            raise GameConfigError("Unable to find Xephyr, install it or disable the Xephyr option")
        xephyr_command = get_xephyr_command(display, self.runner.system_config)
        xephyr_thread = MonitoredCommand(xephyr_command)
        xephyr_thread.start()
        time.sleep(3)
        return display

    def start_antimicrox(self, antimicro_config):
        """Start Antimicrox with a given config path"""
        antimicro_path = system.find_executable("antimicrox")
        if not antimicro_path:
            logger.warning("Antimicrox is not installed.")
            return
        logger.info("Starting Antic")
        antimicro_command = [antimicro_path, "--hidden", "--tray", "--profile", antimicro_config]
        self.antimicro_thread = MonitoredCommand(antimicro_command)
        self.antimicro_thread.start()

    @staticmethod
    def set_keyboard_layout(layout):
        setxkbmap_command = ["setxkbmap", "-model", "pc101", layout, "-print"]
        xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")]
        with subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) as xkbcomp:
            with subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin) as setxkbmap:
                setxkbmap.communicate()
                xkbcomp.communicate()

    def start_prelaunch_command(self, wait_for_completion=False):
        """Start the prelaunch command specified in the system options"""
        prelaunch_command = self.runner.system_config.get("prelaunch_command")
        command_array = shlex.split(prelaunch_command)
        if not system.path_exists(command_array[0]):
            logger.warning("Command %s not found", command_array[0])
            return
        env = self.game_runtime_config["env"]
        if wait_for_completion:
            logger.info("Prelauch command: %s, waiting for completion", prelaunch_command)
            # Monitor the prelaunch command and wait until it has finished
            system.execute(command_array, env=env, cwd=self.directory)
        else:
            logger.info("Prelaunch command %s launched in the background", prelaunch_command)
            self.prelaunch_executor = MonitoredCommand(
                command_array,
                include_processes=[os.path.basename(command_array[0])],
                env=env,
                cwd=self.directory,
            )
            self.prelaunch_executor.start()

    def get_terminal(self):
        """Return the terminal used to run the game into or None if the game is not run from a terminal.
        Remember that only games using text mode should use the terminal.
        """
        if self.runner.system_config.get("terminal"):
            terminal = self.runner.system_config.get("terminal_app", linux.get_default_terminal())
            if terminal and not system.find_executable(terminal):
                raise GameConfigError(_("The selected terminal application could not be launched:\n%s") % terminal)
            return terminal

    def get_killswitch(self):
        """Return the path to a file that is monitored during game execution.
        If the file stops existing, the game is stopped.
        """
        killswitch = self.runner.system_config.get("killswitch")
        # Prevent setting a killswitch to a file that doesn't exists
        if killswitch and system.path_exists(self.killswitch):
            return killswitch

    def get_gameplay_info(self):
        """Return the information provided by a runner's play method.
        Checks for possible errors.
        """
        if not self.runner:
            logger.warning("Trying to launch %s without a runner", self)
            return {}
        gameplay_info = self.runner.play()
        if "error" in gameplay_info:
            self.show_error_message(gameplay_info)
            self.state = self.STATE_STOPPED
            self.emit("game-stop")
            return

        if self.config.game_level.get("game", {}).get("launch_configs"):
            configs = self.config.game_level["game"]["launch_configs"]
            dlg = dialogs.LaunchConfigSelectDialog(self, configs)
            if dlg.config_index:
                config = configs[dlg.config_index - 1]
                if "command" not in gameplay_info:
                    logger.debug("No command in %s", gameplay_info)
                    logger.debug(config)
                    return {}

                gameplay_info["command"] = [gameplay_info["command"][0], config["exe"]]
                if config.get("args"):
                    gameplay_info["command"] += strings.split_arguments(config["args"])

        return gameplay_info

    @watch_lutris_errors
    def configure_game(self, prelaunched, error=None):  # noqa: C901
        """Get the game ready to start, applying all the options
        This methods sets the game_runtime_config attribute.
        """
        if error:
            logger.error(error)
            dialogs.ErrorDialog(str(error))
        if not prelaunched:
            logger.error("Game prelaunch unsuccessful")
            dialogs.ErrorDialog(_("An error prevented the game from running"))
            self.state = self.STATE_STOPPED
            self.emit("game-stop")
            return
        gameplay_info = self.get_gameplay_info()
        if not gameplay_info:
            return
        command, env = get_launch_parameters(self.runner, gameplay_info)
        env["game_name"] = self.name  # What is this used for??
        self.game_runtime_config = {
            "args": command,
            "env": env,
            "terminal": self.get_terminal(),
            "include_processes": shlex.split(self.runner.system_config.get("include_processes", "")),
            "exclude_processes": shlex.split(self.runner.system_config.get("exclude_processes", "")),
        }

        # Audio control
        if self.runner.system_config.get("reset_pulse"):
            audio.reset_pulse()

        # Input control
        if self.runner.system_config.get("use_us_layout"):
            self.set_keyboard_layout("us")

        # Display control
        self.original_outputs = DISPLAY_MANAGER.get_config()

        if self.runner.system_config.get("disable_compositor"):
            self.set_desktop_compositing(False)

        if self.runner.system_config.get("disable_screen_saver"):
            self.screen_saver_inhibitor_cookie = SCREEN_SAVER_INHIBITOR.inhibit(self.name)

        if self.runner.system_config.get("display") != "off":
            self.resolution_changed = self.restrict_to_display(self.runner.system_config.get("display"))

        resolution = self.runner.system_config.get("resolution")
        if resolution != "off":
            DISPLAY_MANAGER.set_resolution(resolution)
            time.sleep(3)
            self.resolution_changed = True

        xephyr = self.runner.system_config.get("xephyr") or "off"
        if xephyr != "off":
            env["DISPLAY"] = self.start_xephyr()

        antimicro_config = self.runner.system_config.get("antimicro_config")
        if system.path_exists(antimicro_config):
            self.start_antimicrox(antimicro_config)

        # Execution control
        self.killswitch = self.get_killswitch()

        if self.runner.system_config.get("prelaunch_command"):
            self.start_prelaunch_command(self.runner.system_config.get("prelaunch_wait"))

        self.start_game()

    def launch(self):
        """Request launching a game. The game may not be installed yet."""
        if not self.is_launchable():
            logger.error("Game is not launchable")
            return

        self.load_config()  # Reload the config before launching it.

        if str(self.id) in LOG_BUFFERS:  # Reset game logs on each launch
            log_buffer = LOG_BUFFERS[str(self.id)]
            log_buffer.delete(log_buffer.get_start_iter(), log_buffer.get_end_iter())

        self.state = self.STATE_LAUNCHING
        self.prelaunch_pids = system.get_running_pid_list()
        self.emit("game-start")
        jobs.AsyncCall(self.runner.prelaunch, self.configure_game)

    def start_game(self):
        """Run a background command to lauch the game"""
        self.game_thread = MonitoredCommand(
            self.game_runtime_config["args"],
            title=self.name,
            runner=self.runner,
            env=self.game_runtime_config["env"],
            term=self.game_runtime_config["terminal"],
            log_buffer=self.log_buffer,
            include_processes=self.game_runtime_config["include_processes"],
            exclude_processes=self.game_runtime_config["exclude_processes"],
        )
        if hasattr(self.runner, "stop"):
            self.game_thread.stop_func = self.runner.stop
        self.game_uuid = self.game_thread.env["LUTRIS_GAME_UUID"]
        self.game_thread.start()
        self.timer.start()
        self.state = self.STATE_RUNNING
        self.emit("game-started")
        self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat)
        with open(self.now_playing_path, "w", encoding="utf-8") as np_file:
            np_file.write(self.name)

    def force_stop(self):
        # If force_stop_game fails, wait a few seconds and try SIGKILL on any survivors
        self.runner.force_stop_game(self)
        if self.get_stop_pids():
            self.force_kill_delayed()
        else:
            self.stop_game()

    def force_kill_delayed(self, death_watch_seconds=5, death_watch_interval_seconds=.5):
        """Forces termination of a running game, but only after a set time has elapsed;
        Invokes stop_game() when the game is dead."""

        def death_watch():
            """Wait for the processes to die; returns True if do they all did."""
            for _n in range(int(death_watch_seconds / death_watch_interval_seconds)):
                time.sleep(death_watch_interval_seconds)
                if not self.get_stop_pids():
                    return True
            return False

        def death_watch_cb(all_died, error):
            """Called after the death watch to more firmly kill any survivors."""
            if error:
                dialogs.ErrorDialog(str(error))
            elif not all_died:
                self.kill_processes(signal.SIGKILL)
            # If we still can't kill everything, we'll still say we stopped it.
            self.stop_game()

        jobs.AsyncCall(death_watch, death_watch_cb)

    def kill_processes(self, sig):
        """Sends a signal to a process list, logging errors."""
        pids = self.get_stop_pids()

        for pid in pids:
            try:
                os.kill(int(pid), sig)
            except ProcessLookupError as ex:
                logger.debug("Failed to kill game process: %s", ex)

    def get_stop_pids(self):
        """Finds the PIDs of processes that need killin'!"""
        pids = self.get_game_pids()
        if self.game_thread and self.game_thread.game_process:
            pids.add(self.game_thread.game_process.pid)
        return pids

    def get_game_pids(self):
        """Return a list of processes belonging to the Lutris game"""
        new_pids = self.get_new_pids()
        game_pids = []
        game_folder = self.runner.game_path or ""
        for pid in new_pids:
            cmdline = Process(pid).cmdline or ""
            # pressure-vessel: This could potentially pick up PIDs not started by lutris?
            if game_folder in cmdline or "pressure-vessel" in cmdline:
                game_pids.append(pid)
        return set(game_pids + [
            pid for pid in new_pids
            if Process(pid).environ.get("LUTRIS_GAME_UUID") == self.game_uuid
        ])

    def get_new_pids(self):
        """Return list of PIDs started since the game was launched"""
        return set(system.get_running_pid_list()) - set(self.prelaunch_pids)

    def stop_game(self):
        """Cleanup after a game as stopped"""
        duration = self.timer.duration
        logger.debug("%s has run for %s seconds", self, duration)
        if duration < 5:
            logger.warning("The game has run for a very short time, did it crash?")
            # Inspect why it could have crashed

        self.state = self.STATE_STOPPED
        self.emit("game-stop")
        if os.path.exists(self.now_playing_path):
            os.unlink(self.now_playing_path)
        if not self.timer.finished:
            self.timer.end()
            self.playtime += self.timer.duration / 3600

    def prelaunch_beat(self):
        """Watch the prelaunch command"""
        if self.prelaunch_executor and self.prelaunch_executor.is_running:
            return True
        self.start_game()
        return False

    def beat(self):
        """Watch the game's process(es)."""
        if self.game_thread.error:
            dialogs.ErrorDialog(_("<b>Error lauching the game:</b>\n") + self.game_thread.error)
            self.on_game_quit()
            return False

        # The killswitch file should be set to a device (ie. /dev/input/js0)
        # When that device is unplugged, the game is forced to quit.
        killswitch_engage = self.killswitch and not system.path_exists(self.killswitch)
        if killswitch_engage:
            logger.warning("File descriptor no longer present, force quit the game")
            self.force_stop()
            return False
        game_pids = self.get_game_pids()
        if not self.game_thread.is_running and not game_pids:
            logger.debug("Game thread stopped")
            self.on_game_quit()
            return False
        return True

    def stop(self):
        """Stops the game"""
        if self.state == self.STATE_STOPPED:
            logger.debug("Game already stopped")
            return

        logger.info("Stopping %s", self)

        if self.game_thread:
            jobs.AsyncCall(self.game_thread.stop, None)
        self.stop_game()

    def on_game_quit(self):
        """Restore some settings and cleanup after game quit."""

        if self.prelaunch_executor and self.prelaunch_executor.is_running:
            logger.info("Stopping prelaunch script")
            self.prelaunch_executor.stop()

        self.heartbeat = None
        if self.state != self.STATE_STOPPED:
            logger.warning("Game still running (state: %s)", self.state)
            self.stop()

        # Check for post game script
        postexit_command = self.runner.system_config.get("postexit_command")
        if postexit_command:
            command_array = shlex.split(postexit_command)
            if system.path_exists(command_array[0]):
                logger.info("Running post-exit command: %s", postexit_command)
                postexit_thread = MonitoredCommand(
                    command_array,
                    include_processes=[os.path.basename(postexit_command)],
                    env=self.game_runtime_config["env"],
                    cwd=self.directory,
                )
                postexit_thread.start()

        quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
        logger.debug("%s stopped at %s", self.name, quit_time)
        self.lastplayed = int(time.time())
        self.save(save_config=False)

        os.chdir(os.path.expanduser("~"))

        if self.antimicro_thread:
            self.antimicro_thread.stop()

        if self.resolution_changed or self.runner.system_config.get("reset_desktop"):
            DISPLAY_MANAGER.set_resolution(self.original_outputs)

        if self.compositor_disabled:
            self.set_desktop_compositing(True)

        if self.screen_saver_inhibitor_cookie is not None:
            SCREEN_SAVER_INHIBITOR.uninhibit(self.screen_saver_inhibitor_cookie)
            self.screen_saver_inhibitor_cookie = None

        if self.runner.system_config.get("use_us_layout"):
            with subprocess.Popen(["setxkbmap"], env=os.environ) as setxkbmap:
                setxkbmap.communicate()

        if self.runner.system_config.get("restore_gamma"):
            restore_gamma()

        self.process_return_codes()

    def process_return_codes(self):
        """Do things depending on how the game quitted."""
        if self.game_thread.return_code == 127:
            # Error missing shared lib
            error = "error while loading shared lib"
            error_line = strings.lookup_string_in_text(error, self.game_thread.stdout)
            if error_line:
                dialogs.ErrorDialog(_("<b>Error: Missing shared library.</b>\n\n%s") % error_line)

        if self.game_thread.return_code == 1:
            # Error Wine version conflict
            error = "maybe the wrong wineserver"
            if strings.lookup_string_in_text(error, self.game_thread.stdout):
                dialogs.ErrorDialog(_("<b>Error: A different Wine version is already using the same Wine prefix.</b>"))

    def write_script(self, script_path):
        """Output the launch argument in a bash script"""
        gameplay_info = self.get_gameplay_info()
        if not gameplay_info:
            logger.error("Unable to retrieve game information for %s. Can't write a script", self)
            return
        export_bash_script(self.runner, gameplay_info, script_path)

    def move(self, new_location):
        logger.info("Moving %s to %s", self, new_location)
        new_config = ""
        old_location = self.directory
        if os.path.exists(old_location):
            game_directory = os.path.basename(old_location)
            target_directory = os.path.join(new_location, game_directory)
        else:
            target_directory = new_location
        self.directory = target_directory
        self.save()
        if not old_location:
            logger.info("Previous location wasn't set. Cannot continue moving")
            return target_directory

        with open(self.config.game_config_path, encoding='utf-8') as config_file:
            for line in config_file.readlines():
                if target_directory in line:
                    new_config += line
                else:
                    new_config += line.replace(old_location, target_directory)
        with open(self.config.game_config_path, "w", encoding='utf-8') as config_file:
            config_file.write(new_config)

        if not system.path_exists(old_location):
            logger.warning("Location %s doesn't exist, files already moved?", old_location)
            return target_directory
        if new_location.startswith(old_location):
            logger.warning("Can't move %s to one of its children %s", old_location, new_location)
            return target_directory
        try:
            shutil.move(old_location, new_location)
        except OSError as ex:
            logger.error(
                "Failed to move %s to %s, you may have to move files manually (Exception: %s)",
                old_location, new_location, ex
            )
        return target_directory

STATE_LAUNCHING

STATE_RUNNING

STATE_STOPPED

formatted_playtime property readonly

Return a human readable formatted play time

is_favorite property readonly

Return whether the game is in the user's favorites

is_updatable property readonly

Return whether the game can be upgraded

log_buffer property readonly

Access the log buffer object, creating it if necessary

now_playing_path

__init__(self, game_id=None) special

Source code in lutris/game.py
def __init__(self, game_id=None):
    super().__init__()
    self.id = game_id  # pylint: disable=invalid-name
    self.runner = None
    self.config = None

    # Load attributes from database
    game_data = games_db.get_game_by_field(game_id, "id")

    self.slug = game_data.get("slug") or ""
    self.runner_name = game_data.get("runner") or ""
    self.directory = game_data.get("directory") or ""
    self.name = game_data.get("name") or ""
    self.game_config_id = game_data.get("configpath") or ""
    self.is_installed = bool(game_data.get("installed") and self.game_config_id)
    self.is_hidden = bool(game_data.get("hidden"))
    self.platform = game_data.get("platform") or ""
    self.year = game_data.get("year") or ""
    self.lastplayed = game_data.get("lastplayed") or 0
    self.has_custom_banner = bool(game_data.get("has_custom_banner"))
    self.has_custom_icon = bool(game_data.get("has_custom_icon"))
    self.service = game_data.get("service")
    self.appid = game_data.get("service_id")
    self.playtime = game_data.get("playtime") or 0.0

    if self.game_config_id:
        self.load_config()
    self.game_uuid = None
    self.game_thread = None
    self.antimicro_thread = None
    self.prelaunch_pids = []
    self.prelaunch_executor = None
    self.heartbeat = None
    self.killswitch = None
    self.state = self.STATE_STOPPED
    self.game_runtime_config = {}
    self.resolution_changed = False
    self.compositor_disabled = False
    self.original_outputs = None
    self._log_buffer = None
    self.timer = Timer()
    self.screen_saver_inhibitor_cookie = None

__repr__(self) special

Source code in lutris/game.py
def __repr__(self):
    return self.__str__()

__str__(self) special

Source code in lutris/game.py
def __str__(self):
    value = self.name or "Game (no name)"
    if self.runner_name:
        value += " (%s)" % self.runner_name
    return value

add_to_favorites(self)

Add the game to the 'favorite' category

Source code in lutris/game.py
def add_to_favorites(self):
    """Add the game to the 'favorite' category"""
    favorite = categories_db.get_category("favorite")
    if not favorite:
        favorite = categories_db.add_category("favorite")
    categories_db.add_game_to_category(self.id, favorite["id"])
    self.emit("game-updated")

beat(self)

Watch the game's process(es).

Source code in lutris/game.py
def beat(self):
    """Watch the game's process(es)."""
    if self.game_thread.error:
        dialogs.ErrorDialog(_("<b>Error lauching the game:</b>\n") + self.game_thread.error)
        self.on_game_quit()
        return False

    # The killswitch file should be set to a device (ie. /dev/input/js0)
    # When that device is unplugged, the game is forced to quit.
    killswitch_engage = self.killswitch and not system.path_exists(self.killswitch)
    if killswitch_engage:
        logger.warning("File descriptor no longer present, force quit the game")
        self.force_stop()
        return False
    game_pids = self.get_game_pids()
    if not self.game_thread.is_running and not game_pids:
        logger.debug("Game thread stopped")
        self.on_game_quit()
        return False
    return True

configure_game(self, prelaunched, error=None)

Get the game ready to start, applying all the options This methods sets the game_runtime_config attribute.

Source code in lutris/game.py
@watch_lutris_errors
def configure_game(self, prelaunched, error=None):  # noqa: C901
    """Get the game ready to start, applying all the options
    This methods sets the game_runtime_config attribute.
    """
    if error:
        logger.error(error)
        dialogs.ErrorDialog(str(error))
    if not prelaunched:
        logger.error("Game prelaunch unsuccessful")
        dialogs.ErrorDialog(_("An error prevented the game from running"))
        self.state = self.STATE_STOPPED
        self.emit("game-stop")
        return
    gameplay_info = self.get_gameplay_info()
    if not gameplay_info:
        return
    command, env = get_launch_parameters(self.runner, gameplay_info)
    env["game_name"] = self.name  # What is this used for??
    self.game_runtime_config = {
        "args": command,
        "env": env,
        "terminal": self.get_terminal(),
        "include_processes": shlex.split(self.runner.system_config.get("include_processes", "")),
        "exclude_processes": shlex.split(self.runner.system_config.get("exclude_processes", "")),
    }

    # Audio control
    if self.runner.system_config.get("reset_pulse"):
        audio.reset_pulse()

    # Input control
    if self.runner.system_config.get("use_us_layout"):
        self.set_keyboard_layout("us")

    # Display control
    self.original_outputs = DISPLAY_MANAGER.get_config()

    if self.runner.system_config.get("disable_compositor"):
        self.set_desktop_compositing(False)

    if self.runner.system_config.get("disable_screen_saver"):
        self.screen_saver_inhibitor_cookie = SCREEN_SAVER_INHIBITOR.inhibit(self.name)

    if self.runner.system_config.get("display") != "off":
        self.resolution_changed = self.restrict_to_display(self.runner.system_config.get("display"))

    resolution = self.runner.system_config.get("resolution")
    if resolution != "off":
        DISPLAY_MANAGER.set_resolution(resolution)
        time.sleep(3)
        self.resolution_changed = True

    xephyr = self.runner.system_config.get("xephyr") or "off"
    if xephyr != "off":
        env["DISPLAY"] = self.start_xephyr()

    antimicro_config = self.runner.system_config.get("antimicro_config")
    if system.path_exists(antimicro_config):
        self.start_antimicrox(antimicro_config)

    # Execution control
    self.killswitch = self.get_killswitch()

    if self.runner.system_config.get("prelaunch_command"):
        self.start_prelaunch_command(self.runner.system_config.get("prelaunch_wait"))

    self.start_game()

delete(self)

Completely remove a game from the library

Source code in lutris/game.py
def delete(self):
    """Completely remove a game from the library"""
    if self.is_installed:
        raise RuntimeError("Uninstall the game before deleting")
    games_db.delete_game(self.id)
    self.emit("game-removed")

force_kill_delayed(self, death_watch_seconds=5, death_watch_interval_seconds=0.5)

Forces termination of a running game, but only after a set time has elapsed; Invokes stop_game() when the game is dead.

Source code in lutris/game.py
def force_kill_delayed(self, death_watch_seconds=5, death_watch_interval_seconds=.5):
    """Forces termination of a running game, but only after a set time has elapsed;
    Invokes stop_game() when the game is dead."""

    def death_watch():
        """Wait for the processes to die; returns True if do they all did."""
        for _n in range(int(death_watch_seconds / death_watch_interval_seconds)):
            time.sleep(death_watch_interval_seconds)
            if not self.get_stop_pids():
                return True
        return False

    def death_watch_cb(all_died, error):
        """Called after the death watch to more firmly kill any survivors."""
        if error:
            dialogs.ErrorDialog(str(error))
        elif not all_died:
            self.kill_processes(signal.SIGKILL)
        # If we still can't kill everything, we'll still say we stopped it.
        self.stop_game()

    jobs.AsyncCall(death_watch, death_watch_cb)

force_stop(self)

Source code in lutris/game.py
def force_stop(self):
    # If force_stop_game fails, wait a few seconds and try SIGKILL on any survivors
    self.runner.force_stop_game(self)
    if self.get_stop_pids():
        self.force_kill_delayed()
    else:
        self.stop_game()

get_browse_dir(self)

Return the path to open with the Browse Files action.

Source code in lutris/game.py
def get_browse_dir(self):
    """Return the path to open with the Browse Files action."""
    return self.runner.game_path

get_game_pids(self)

Return a list of processes belonging to the Lutris game

Source code in lutris/game.py
def get_game_pids(self):
    """Return a list of processes belonging to the Lutris game"""
    new_pids = self.get_new_pids()
    game_pids = []
    game_folder = self.runner.game_path or ""
    for pid in new_pids:
        cmdline = Process(pid).cmdline or ""
        # pressure-vessel: This could potentially pick up PIDs not started by lutris?
        if game_folder in cmdline or "pressure-vessel" in cmdline:
            game_pids.append(pid)
    return set(game_pids + [
        pid for pid in new_pids
        if Process(pid).environ.get("LUTRIS_GAME_UUID") == self.game_uuid
    ])

get_gameplay_info(self)

Return the information provided by a runner's play method. Checks for possible errors.

Source code in lutris/game.py
def get_gameplay_info(self):
    """Return the information provided by a runner's play method.
    Checks for possible errors.
    """
    if not self.runner:
        logger.warning("Trying to launch %s without a runner", self)
        return {}
    gameplay_info = self.runner.play()
    if "error" in gameplay_info:
        self.show_error_message(gameplay_info)
        self.state = self.STATE_STOPPED
        self.emit("game-stop")
        return

    if self.config.game_level.get("game", {}).get("launch_configs"):
        configs = self.config.game_level["game"]["launch_configs"]
        dlg = dialogs.LaunchConfigSelectDialog(self, configs)
        if dlg.config_index:
            config = configs[dlg.config_index - 1]
            if "command" not in gameplay_info:
                logger.debug("No command in %s", gameplay_info)
                logger.debug(config)
                return {}

            gameplay_info["command"] = [gameplay_info["command"][0], config["exe"]]
            if config.get("args"):
                gameplay_info["command"] += strings.split_arguments(config["args"])

    return gameplay_info

get_killswitch(self)

Return the path to a file that is monitored during game execution. If the file stops existing, the game is stopped.

Source code in lutris/game.py
def get_killswitch(self):
    """Return the path to a file that is monitored during game execution.
    If the file stops existing, the game is stopped.
    """
    killswitch = self.runner.system_config.get("killswitch")
    # Prevent setting a killswitch to a file that doesn't exists
    if killswitch and system.path_exists(self.killswitch):
        return killswitch

get_new_pids(self)

Return list of PIDs started since the game was launched

Source code in lutris/game.py
def get_new_pids(self):
    """Return list of PIDs started since the game was launched"""
    return set(system.get_running_pid_list()) - set(self.prelaunch_pids)

get_stop_pids(self)

Finds the PIDs of processes that need killin'!

Source code in lutris/game.py
def get_stop_pids(self):
    """Finds the PIDs of processes that need killin'!"""
    pids = self.get_game_pids()
    if self.game_thread and self.game_thread.game_process:
        pids.add(self.game_thread.game_process.pid)
    return pids

get_terminal(self)

Return the terminal used to run the game into or None if the game is not run from a terminal. Remember that only games using text mode should use the terminal.

Source code in lutris/game.py
def get_terminal(self):
    """Return the terminal used to run the game into or None if the game is not run from a terminal.
    Remember that only games using text mode should use the terminal.
    """
    if self.runner.system_config.get("terminal"):
        terminal = self.runner.system_config.get("terminal_app", linux.get_default_terminal())
        if terminal and not system.find_executable(terminal):
            raise GameConfigError(_("The selected terminal application could not be launched:\n%s") % terminal)
        return terminal

is_launchable(self)

Verify that the current game can be launched.

Source code in lutris/game.py
def is_launchable(self):
    """Verify that the current game can be launched."""
    if not self.is_installed:
        logger.error("%s (%s) not installed", self, self.id)
        dialogs.ErrorDialog(_("Tried to launch a game that isn't installed."))
        return False
    if not self.runner:
        dialogs.ErrorDialog(_("Invalid game configuration: Missing runner"))
        return False
    if not self.runner.is_installed():
        installed = self.runner.install_dialog()
        if not installed:
            dialogs.ErrorDialog(_("Runner not installed."))
            return False

    if self.runner.use_runtime():
        runtime_updater = runtime.RuntimeUpdater()
        if runtime_updater.is_updating():
            dialogs.ErrorDialog(_("Runtime currently updating"), _("Game might not work as expected"))
    if ("wine" in self.runner_name and not wine.get_wine_version() and not LINUX_SYSTEM.is_flatpak):
        dialogs.WineNotInstalledWarning(parent=None)
    return True

kill_processes(self, sig)

Sends a signal to a process list, logging errors.

Source code in lutris/game.py
def kill_processes(self, sig):
    """Sends a signal to a process list, logging errors."""
    pids = self.get_stop_pids()

    for pid in pids:
        try:
            os.kill(int(pid), sig)
        except ProcessLookupError as ex:
            logger.debug("Failed to kill game process: %s", ex)

launch(self)

Request launching a game. The game may not be installed yet.

Source code in lutris/game.py
def launch(self):
    """Request launching a game. The game may not be installed yet."""
    if not self.is_launchable():
        logger.error("Game is not launchable")
        return

    self.load_config()  # Reload the config before launching it.

    if str(self.id) in LOG_BUFFERS:  # Reset game logs on each launch
        log_buffer = LOG_BUFFERS[str(self.id)]
        log_buffer.delete(log_buffer.get_start_iter(), log_buffer.get_end_iter())

    self.state = self.STATE_LAUNCHING
    self.prelaunch_pids = system.get_running_pid_list()
    self.emit("game-start")
    jobs.AsyncCall(self.runner.prelaunch, self.configure_game)

load_config(self)

Load the game's configuration.

Source code in lutris/game.py
def load_config(self):
    """Load the game's configuration."""
    if not self.is_installed:
        return
    self.config = LutrisConfig(runner_slug=self.runner_name, game_config_id=self.game_config_id)
    self.runner = self._get_runner()

move(self, new_location)

Source code in lutris/game.py
def move(self, new_location):
    logger.info("Moving %s to %s", self, new_location)
    new_config = ""
    old_location = self.directory
    if os.path.exists(old_location):
        game_directory = os.path.basename(old_location)
        target_directory = os.path.join(new_location, game_directory)
    else:
        target_directory = new_location
    self.directory = target_directory
    self.save()
    if not old_location:
        logger.info("Previous location wasn't set. Cannot continue moving")
        return target_directory

    with open(self.config.game_config_path, encoding='utf-8') as config_file:
        for line in config_file.readlines():
            if target_directory in line:
                new_config += line
            else:
                new_config += line.replace(old_location, target_directory)
    with open(self.config.game_config_path, "w", encoding='utf-8') as config_file:
        config_file.write(new_config)

    if not system.path_exists(old_location):
        logger.warning("Location %s doesn't exist, files already moved?", old_location)
        return target_directory
    if new_location.startswith(old_location):
        logger.warning("Can't move %s to one of its children %s", old_location, new_location)
        return target_directory
    try:
        shutil.move(old_location, new_location)
    except OSError as ex:
        logger.error(
            "Failed to move %s to %s, you may have to move files manually (Exception: %s)",
            old_location, new_location, ex
        )
    return target_directory

on_game_quit(self)

Restore some settings and cleanup after game quit.

Source code in lutris/game.py
def on_game_quit(self):
    """Restore some settings and cleanup after game quit."""

    if self.prelaunch_executor and self.prelaunch_executor.is_running:
        logger.info("Stopping prelaunch script")
        self.prelaunch_executor.stop()

    self.heartbeat = None
    if self.state != self.STATE_STOPPED:
        logger.warning("Game still running (state: %s)", self.state)
        self.stop()

    # Check for post game script
    postexit_command = self.runner.system_config.get("postexit_command")
    if postexit_command:
        command_array = shlex.split(postexit_command)
        if system.path_exists(command_array[0]):
            logger.info("Running post-exit command: %s", postexit_command)
            postexit_thread = MonitoredCommand(
                command_array,
                include_processes=[os.path.basename(postexit_command)],
                env=self.game_runtime_config["env"],
                cwd=self.directory,
            )
            postexit_thread.start()

    quit_time = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime())
    logger.debug("%s stopped at %s", self.name, quit_time)
    self.lastplayed = int(time.time())
    self.save(save_config=False)

    os.chdir(os.path.expanduser("~"))

    if self.antimicro_thread:
        self.antimicro_thread.stop()

    if self.resolution_changed or self.runner.system_config.get("reset_desktop"):
        DISPLAY_MANAGER.set_resolution(self.original_outputs)

    if self.compositor_disabled:
        self.set_desktop_compositing(True)

    if self.screen_saver_inhibitor_cookie is not None:
        SCREEN_SAVER_INHIBITOR.uninhibit(self.screen_saver_inhibitor_cookie)
        self.screen_saver_inhibitor_cookie = None

    if self.runner.system_config.get("use_us_layout"):
        with subprocess.Popen(["setxkbmap"], env=os.environ) as setxkbmap:
            setxkbmap.communicate()

    if self.runner.system_config.get("restore_gamma"):
        restore_gamma()

    self.process_return_codes()

prelaunch_beat(self)

Watch the prelaunch command

Source code in lutris/game.py
def prelaunch_beat(self):
    """Watch the prelaunch command"""
    if self.prelaunch_executor and self.prelaunch_executor.is_running:
        return True
    self.start_game()
    return False

process_return_codes(self)

Do things depending on how the game quitted.

Source code in lutris/game.py
def process_return_codes(self):
    """Do things depending on how the game quitted."""
    if self.game_thread.return_code == 127:
        # Error missing shared lib
        error = "error while loading shared lib"
        error_line = strings.lookup_string_in_text(error, self.game_thread.stdout)
        if error_line:
            dialogs.ErrorDialog(_("<b>Error: Missing shared library.</b>\n\n%s") % error_line)

    if self.game_thread.return_code == 1:
        # Error Wine version conflict
        error = "maybe the wrong wineserver"
        if strings.lookup_string_in_text(error, self.game_thread.stdout):
            dialogs.ErrorDialog(_("<b>Error: A different Wine version is already using the same Wine prefix.</b>"))

remove(self, delete_files=False, no_signal=False)

Uninstall a game

Parameters:

Name Type Description Default
delete_files bool

Delete the game files

False
no_signal bool

Don't emit game-removed signal (if running in a thread)

False
Source code in lutris/game.py
def remove(self, delete_files=False, no_signal=False):
    """Uninstall a game

    Params:
        delete_files (bool): Delete the game files
        no_signal (bool): Don't emit game-removed signal (if running in a thread)
    """
    sql.db_update(settings.PGA_DB, "games", {"installed": 0, "runner": ""}, {"id": self.id})
    if self.config:
        self.config.remove()
    xdgshortcuts.remove_launcher(self.slug, self.id, desktop=True, menu=True)
    if delete_files and self.runner:
        self.runner.remove_game_data(game_path=self.directory)
    self.is_installed = False
    self.runner = None
    if no_signal:
        return
    self.emit("game-removed")

remove_from_favorites(self)

Remove game from favorites

Source code in lutris/game.py
def remove_from_favorites(self):
    """Remove game from favorites"""
    favorite = categories_db.get_category("favorite")
    categories_db.remove_category_from_game(self.id, favorite["id"])
    self.emit("game-updated")

restrict_to_display(self, display)

Source code in lutris/game.py
def restrict_to_display(self, display):
    outputs = DISPLAY_MANAGER.get_config()
    if display == "primary":
        display = None
        for output in outputs:
            if output.primary:
                display = output.name
                break
        if not display:
            logger.warning("No primary display set")
    else:
        found = False
        for output in outputs:
            if output.name == display:
                found = True
                break
        if not found:
            logger.warning("Selected display %s not found", display)
            display = None
    if display:
        turn_off_except(display)
        time.sleep(3)
        return True
    return False

save(self, save_config=False)

Save the game's config and metadata, if save_config is set to False, do not save the config. This is useful when exiting the game since the config might have changed and we don't want to override the changes.

Source code in lutris/game.py
def save(self, save_config=False):
    """
    Save the game's config and metadata, if `save_config` is set to False,
    do not save the config. This is useful when exiting the game since the
    config might have changed and we don't want to override the changes.
    """
    if self.config:
        logger.debug("Saving %s with config ID %s", self, self.config.game_config_id)
        configpath = self.config.game_config_id
        if save_config:
            self.config.save()
    else:
        logger.warning("Saving %s without a configuration", self)
        configpath = ""
    self.set_platform_from_runner()
    self.id = games_db.add_or_update(
        name=self.name,
        runner=self.runner_name,
        slug=self.slug,
        platform=self.platform,
        directory=self.directory,
        installed=self.is_installed,
        year=self.year,
        lastplayed=self.lastplayed,
        configpath=configpath,
        id=self.id,
        playtime=self.playtime,
        hidden=self.is_hidden,
        service=self.service,
        service_id=self.appid,
    )
    self.emit("game-updated")

set_desktop_compositing(self, enable)

Enables or disables compositing

Source code in lutris/game.py
def set_desktop_compositing(self, enable):
    """Enables or disables compositing"""
    if enable:
        if self.compositor_disabled:
            enable_compositing()
            self.compositor_disabled = False
    else:
        if not self.compositor_disabled:
            disable_compositing()
            self.compositor_disabled = True

set_hidden(self, is_hidden)

Do not show this game in the UI

Source code in lutris/game.py
def set_hidden(self, is_hidden):
    """Do not show this game in the UI"""
    self.is_hidden = is_hidden
    self.save()
    self.emit("game-updated")

set_keyboard_layout(layout) staticmethod

Source code in lutris/game.py
@staticmethod
def set_keyboard_layout(layout):
    setxkbmap_command = ["setxkbmap", "-model", "pc101", layout, "-print"]
    xkbcomp_command = ["xkbcomp", "-", os.environ.get("DISPLAY", ":0")]
    with subprocess.Popen(xkbcomp_command, stdin=subprocess.PIPE) as xkbcomp:
        with subprocess.Popen(setxkbmap_command, env=os.environ, stdout=xkbcomp.stdin) as setxkbmap:
            setxkbmap.communicate()
            xkbcomp.communicate()

set_platform_from_runner(self)

Set the game's platform from the runner

Source code in lutris/game.py
def set_platform_from_runner(self):
    """Set the game's platform from the runner"""
    if not self.runner:
        logger.warning("Game has no runner, can't set platform")
        return
    self.platform = self.runner.get_platform()
    if not self.platform:
        logger.warning("The %s runner didn't provide a platform for %s", self.runner.human_name, self)

show_error_message(message) staticmethod

Display an error message based on the runner's output.

Source code in lutris/game.py
@staticmethod
def show_error_message(message):
    """Display an error message based on the runner's output."""
    if message["error"] == "CUSTOM":
        message_text = message["text"].replace("&", "&amp;")
        dialogs.ErrorDialog(message_text)
    elif message["error"] == "RUNNER_NOT_INSTALLED":
        dialogs.ErrorDialog(_("Error the runner is not installed"))
    elif message["error"] == "NO_BIOS":
        dialogs.ErrorDialog(_("A bios file is required to run this game"))
    elif message["error"] == "FILE_NOT_FOUND":
        filename = message["file"]
        if filename:
            message_text = _("The file {} could not be found").format(filename.replace("&", "&amp;"))
        else:
            message_text = _("This game has no executable set. The install process didn't finish properly.")
        dialogs.ErrorDialog(message_text)
    elif message["error"] == "NOT_EXECUTABLE":
        message_text = message["file"].replace("&", "&amp;")
        dialogs.ErrorDialog(_("The file %s is not executable") % message_text)
    elif message["error"] == "PATH_NOT_SET":
        message_text = _("The path '%s' is not set. please set it in the options.") % message["path"]
        dialogs.ErrorDialog(message_text)
    else:
        dialogs.ErrorDialog(_("Unhandled error: %s") % message["error"])

start_antimicrox(self, antimicro_config)

Start Antimicrox with a given config path

Source code in lutris/game.py
def start_antimicrox(self, antimicro_config):
    """Start Antimicrox with a given config path"""
    antimicro_path = system.find_executable("antimicrox")
    if not antimicro_path:
        logger.warning("Antimicrox is not installed.")
        return
    logger.info("Starting Antic")
    antimicro_command = [antimicro_path, "--hidden", "--tray", "--profile", antimicro_config]
    self.antimicro_thread = MonitoredCommand(antimicro_command)
    self.antimicro_thread.start()

start_game(self)

Run a background command to lauch the game

Source code in lutris/game.py
def start_game(self):
    """Run a background command to lauch the game"""
    self.game_thread = MonitoredCommand(
        self.game_runtime_config["args"],
        title=self.name,
        runner=self.runner,
        env=self.game_runtime_config["env"],
        term=self.game_runtime_config["terminal"],
        log_buffer=self.log_buffer,
        include_processes=self.game_runtime_config["include_processes"],
        exclude_processes=self.game_runtime_config["exclude_processes"],
    )
    if hasattr(self.runner, "stop"):
        self.game_thread.stop_func = self.runner.stop
    self.game_uuid = self.game_thread.env["LUTRIS_GAME_UUID"]
    self.game_thread.start()
    self.timer.start()
    self.state = self.STATE_RUNNING
    self.emit("game-started")
    self.heartbeat = GLib.timeout_add(HEARTBEAT_DELAY, self.beat)
    with open(self.now_playing_path, "w", encoding="utf-8") as np_file:
        np_file.write(self.name)

start_prelaunch_command(self, wait_for_completion=False)

Start the prelaunch command specified in the system options

Source code in lutris/game.py
def start_prelaunch_command(self, wait_for_completion=False):
    """Start the prelaunch command specified in the system options"""
    prelaunch_command = self.runner.system_config.get("prelaunch_command")
    command_array = shlex.split(prelaunch_command)
    if not system.path_exists(command_array[0]):
        logger.warning("Command %s not found", command_array[0])
        return
    env = self.game_runtime_config["env"]
    if wait_for_completion:
        logger.info("Prelauch command: %s, waiting for completion", prelaunch_command)
        # Monitor the prelaunch command and wait until it has finished
        system.execute(command_array, env=env, cwd=self.directory)
    else:
        logger.info("Prelaunch command %s launched in the background", prelaunch_command)
        self.prelaunch_executor = MonitoredCommand(
            command_array,
            include_processes=[os.path.basename(command_array[0])],
            env=env,
            cwd=self.directory,
        )
        self.prelaunch_executor.start()

start_xephyr(self, display=':2')

Start a monitored Xephyr instance

Source code in lutris/game.py
def start_xephyr(self, display=":2"):
    """Start a monitored Xephyr instance"""
    if not system.find_executable("Xephyr"):
        raise GameConfigError("Unable to find Xephyr, install it or disable the Xephyr option")
    xephyr_command = get_xephyr_command(display, self.runner.system_config)
    xephyr_thread = MonitoredCommand(xephyr_command)
    xephyr_thread.start()
    time.sleep(3)
    return display

stop(self)

Stops the game

Source code in lutris/game.py
def stop(self):
    """Stops the game"""
    if self.state == self.STATE_STOPPED:
        logger.debug("Game already stopped")
        return

    logger.info("Stopping %s", self)

    if self.game_thread:
        jobs.AsyncCall(self.game_thread.stop, None)
    self.stop_game()

stop_game(self)

Cleanup after a game as stopped

Source code in lutris/game.py
def stop_game(self):
    """Cleanup after a game as stopped"""
    duration = self.timer.duration
    logger.debug("%s has run for %s seconds", self, duration)
    if duration < 5:
        logger.warning("The game has run for a very short time, did it crash?")
        # Inspect why it could have crashed

    self.state = self.STATE_STOPPED
    self.emit("game-stop")
    if os.path.exists(self.now_playing_path):
        os.unlink(self.now_playing_path)
    if not self.timer.finished:
        self.timer.end()
        self.playtime += self.timer.duration / 3600

write_script(self, script_path)

Output the launch argument in a bash script

Source code in lutris/game.py
def write_script(self, script_path):
    """Output the launch argument in a bash script"""
    gameplay_info = self.get_gameplay_info()
    if not gameplay_info:
        logger.error("Unable to retrieve game information for %s. Can't write a script", self)
        return
    export_bash_script(self.runner, gameplay_info, script_path)

export_game(slug, dest_dir)

Export a full game folder along with some lutris metadata

Source code in lutris/game.py
def export_game(slug, dest_dir):
    """Export a full game folder along with some lutris metadata"""

    # List of runner where we know for sure that 1 folder = 1 game.
    # For runners that handle ROMs, we have to handle this more finely.
    # There is likely more than one game in a ROM folder but a ROM
    # might have several files (like a bin/cue, or a multi-disk game)
    exportable_runners = [
        "linux",
        "wine",
        "dosbox",
        "scummvm",
    ]

    db_game = games_db.get_game_by_field(slug, "slug")
    if db_game["runner"] not in exportable_runners:
        raise RuntimeError("Game %s can't be exported." % db_game["name"])
    if not db_game["directory"]:
        raise RuntimeError("No game directory set. Could we guess it?")

    game = Game(db_game["id"])
    db_game["config"] = game.config.game_level
    game_path = db_game["directory"]
    config_path = os.path.join(db_game["directory"], "%s.lutris" % slug)
    with open(config_path, "w", encoding="utf-8") as config_file:
        json.dump(db_game, config_file, indent=2)
    archive_path = os.path.join(dest_dir, "%s.7z" % slug)
    _7zip_path = os.path.join(settings.RUNTIME_DIR, "p7zip/7z")
    command = [_7zip_path, "a", archive_path, game_path]
    return_code = subprocess.call(command)
    if return_code != 0:
        print("Creating of archive in %s failed with return code %s" % (archive_path, return_code))

import_game(file_path, dest_dir)

Import a game in Lutris

Source code in lutris/game.py
def import_game(file_path, dest_dir):
    """Import a game in Lutris"""
    if not os.path.exists(file_path):
        raise RuntimeError("No file %s" % file_path)
    if not os.path.isdir(dest_dir):
        os.makedirs(dest_dir)
    original_file_list = set(os.listdir(dest_dir))
    extract.extract_7zip(file_path, dest_dir)
    new_file_list = set(os.listdir(dest_dir))
    new_dir = list(new_file_list - original_file_list)[0]
    game_dir = os.path.join(dest_dir, new_dir)
    game_config = [f for f in os.listdir(game_dir) if f.endswith(".lutris")][0]
    with open(os.path.join(game_dir, game_config)) as config_file:
        lutris_config = json.load(config_file)
    # old_dir = lutris_config["directory"]
    config_filename = os.path.join(settings.CONFIG_DIR, "games/%s.yml" % lutris_config["configpath"])
    write_yaml_to_file(lutris_config["config"], config_filename)
    game_id = games_db.add_or_update(
        name=lutris_config["name"],
        runner=lutris_config["runner"],
        slug=lutris_config["slug"],
        platform=lutris_config["platform"],
        directory=game_dir,
        installed=lutris_config["installed"],
        year=lutris_config["year"],
        lastplayed=lutris_config["lastplayed"],
        configpath=lutris_config["configpath"],
        playtime=lutris_config["playtime"],
        hidden=lutris_config["hidden"],
        service=lutris_config["service"],
        service_id=lutris_config["service_id"],
    )
    print("Added game with ID %s" % game_id)

game_actions

Handle game specific actions

GameActions

Regroup a list of callbacks for a game

Source code in lutris/game_actions.py
class GameActions:
    """Regroup a list of callbacks for a game"""

    def __init__(self, application=None, window=None):
        self.application = application or Gio.Application.get_default()
        self.window = window
        self.game_id = None
        self._game = None

    @property
    def game(self):
        if not self._game:
            self._game = self.application.get_game_by_id(self.game_id)
            if not self._game:
                self._game = Game(self.game_id)
            self._game.connect("game-error", self.window.on_game_error)
        return self._game

    @property
    def is_game_running(self):
        return bool(self.application.get_game_by_id(self.game_id))

    def set_game(self, game=None, game_id=None):
        if game:
            self._game = game
            self.game_id = game.id
        else:
            self._game = None
            self.game_id = game_id

    def get_game_actions(self):
        """Return a list of game actions and their callbacks"""
        return [
            ("play", _("Play"), self.on_game_launch),
            ("stop", _("Stop"), self.on_game_stop),
            ("install", _("Install"), self.on_install_clicked),
            ("update", _("Install updates"), self.on_update_clicked),
            ("install_dlcs", "Install DLCs", self.on_install_dlc_clicked),
            ("show_logs", _("Show logs"), self.on_show_logs),
            ("add", _("Add installed game"), self.on_add_manually),
            ("duplicate", _("Duplicate"), self.on_game_duplicate),
            ("configure", _("Configure"), self.on_edit_game_configuration),
            ("favorite", _("Add to favorites"), self.on_add_favorite_game),
            ("deletefavorite", _("Remove from favorites"), self.on_delete_favorite_game),
            ("execute-script", _("Execute script"), self.on_execute_script_clicked),
            ("browse", _("Browse files"), self.on_browse_files),
            (
                "desktop-shortcut",
                _("Create desktop shortcut"),
                self.on_create_desktop_shortcut,
            ),
            (
                "rm-desktop-shortcut",
                _("Delete desktop shortcut"),
                self.on_remove_desktop_shortcut,
            ),
            (
                "menu-shortcut",
                _("Create application menu shortcut"),
                self.on_create_menu_shortcut,
            ),
            (
                "rm-menu-shortcut",
                _("Delete application menu shortcut"),
                self.on_remove_menu_shortcut,
            ),
            ("install_more", _("Install another version"), self.on_install_clicked),
            ("remove", _("Remove"), self.on_remove_game),
            ("view", _("View on Lutris.net"), self.on_view_game),
            ("hide", _("Hide game from library"), self.on_hide_game),
            ("unhide", _("Unhide game from library"), self.on_unhide_game),
        ]

    def get_displayed_entries(self):
        """Return a dictionary of actions that should be shown for a game"""
        return {
            "add": not self.game.is_installed,
            "duplicate": True,
            "install": not self.game.is_installed,
            "play": self.game.is_installed and not self.is_game_running,
            "update": self.game.is_updatable,
            "install_dlcs": self.game.is_updatable,
            "stop": self.is_game_running,
            "configure": bool(self.game.is_installed),
            "browse": self.game.is_installed and self.game.runner_name != "browser",
            "show_logs": self.game.is_installed,
            "favorite": not self.game.is_favorite,
            "deletefavorite": self.game.is_favorite,
            "install_more": not self.game.service and self.game.is_installed,
            "execute-script": bool(
                self.game.is_installed and self.game.runner
                and self.game.runner.system_config.get("manual_command")
            ),
            "desktop-shortcut": (
                self.game.is_installed
                and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
            ),
            "menu-shortcut": (
                self.game.is_installed
                and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
            ),
            "rm-desktop-shortcut": bool(
                self.game.is_installed
                and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
            ),
            "rm-menu-shortcut": bool(
                self.game.is_installed
                and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
            ),
            "remove": True,
            "view": True,
            "hide": self.game.is_installed and not self.game.is_hidden,
            "unhide": self.game.is_hidden,
        }

    def on_game_launch(self, *_args):
        """Launch a game"""
        self.game.launch()

    def get_running_game(self):
        ids = self.application.get_running_game_ids()
        for game_id in ids:
            if str(game_id) == str(self.game.id):
                return self.game
        logger.warning("Game %s not in %s", self.game_id, ids)

    def on_game_stop(self, _caller):
        """Stops the game"""
        game = self.get_running_game()
        if game:
            game.force_stop()

    def on_show_logs(self, _widget):
        """Display game log"""
        _buffer = self.game.log_buffer
        if not _buffer:
            logger.info("No log for game %s", self.game)
        return LogWindow(
            title=_("Log for {}").format(self.game),
            buffer=_buffer,
            application=self.application
        )

    def on_install_clicked(self, *_args):
        """Install a game"""
        # Install the currently selected game in the UI
        if not self.game.slug:
            raise RuntimeError("No game to install: %s" % self.game.id)
        self.game.emit("game-install")

    def on_update_clicked(self, _widget):
        self.game.emit("game-install-update")

    def on_install_dlc_clicked(self, _widget):
        self.game.emit("game-install-dlc")

    def on_locate_installed_game(self, _button, game):
        """Show the user a dialog to import an existing install to a DRM free service

        Params:
            game (Game): Game instance without a database ID, populated with a fields the service can provides
        """
        AddGameDialog(self.window, game=game)

    def on_add_manually(self, _widget, *_args):
        """Callback that presents the Add game dialog"""
        return AddGameDialog(self.window, game=self.game, runner=self.game.runner_name)

    def on_game_duplicate(self, _widget):
        confirm_dlg = QuestionDialog(
            {
                "parent": self.window,
                "question": _(
                    "Do you wish to duplicate %s?\nThe configuration will be duplicated, "
                    "but the games files will <b>not be duplicated</b>."
                ) % gtk_safe(self.game.name),
                "title": _("Duplicate game?"),
            }
        )
        if confirm_dlg.result != Gtk.ResponseType.YES:
            return

        assigned_name = get_unusued_game_name(self.game.name)
        old_config_id = self.game.game_config_id
        if old_config_id:
            new_config_id = duplicate_game_config(self.game.slug, old_config_id)
        else:
            new_config_id = None

        db_game = get_game_by_field(self.game.id, "id")
        db_game["name"] = assigned_name
        db_game["configpath"] = new_config_id
        db_game.pop("id")
        # Disconnect duplicate from service- there should be at most
        # 1 PGA game for a service game.
        db_game.pop("service", None)
        db_game.pop("service_id", None)

        game_id = add_game(**db_game)
        new_game = Game(game_id)
        new_game.save()

    def on_edit_game_configuration(self, _widget):
        """Edit game preferences"""
        self.application.show_window(EditGameConfigDialog, game=self.game, parent=self.window)

    def on_add_favorite_game(self, _widget):
        """Add to favorite Games list"""
        self.game.add_to_favorites()

    def on_delete_favorite_game(self, _widget):
        """delete from favorites"""
        self.game.remove_from_favorites()

    def on_hide_game(self, _widget):
        """Add a game to the list of hidden games"""
        self.game.set_hidden(True)

    def on_unhide_game(self, _widget):
        """Removes a game from the list of hidden games"""
        self.game.set_hidden(False)

    def on_execute_script_clicked(self, _widget):
        """Execute the game's associated script"""
        manual_command = self.game.runner.system_config.get("manual_command")
        if path_exists(manual_command):
            MonitoredCommand(
                [manual_command],
                include_processes=[os.path.basename(manual_command)],
                cwd=self.game.directory,
            ).start()
            logger.info("Running %s in the background", manual_command)

    def on_browse_files(self, _widget):
        """Callback to open a game folder in the file browser"""
        path = self.game.get_browse_dir()
        if not path:
            dialogs.NoticeDialog(_("This game has no installation directory"))
        elif path_exists(path):
            open_uri("file://%s" % path)
        else:
            dialogs.NoticeDialog(_("Can't open %s \nThe folder doesn't exist.") % path)

    def on_create_menu_shortcut(self, *_args):
        """Add the selected game to the system's Games menu."""
        xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True)

    def on_create_desktop_shortcut(self, *_args):
        """Create a desktop launcher for the selected game."""
        xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True)

    def on_remove_menu_shortcut(self, *_args):
        """Remove an XDG menu shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)

    def on_remove_desktop_shortcut(self, *_args):
        """Remove a .desktop shortcut"""
        xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True)

    def on_view_game(self, _widget):
        """Callback to open a game on lutris.net"""
        open_uri("https://lutris.net/games/%s" % self.game.slug)

    def on_remove_game(self, *_args):
        """Callback that present the uninstall dialog to the user"""
        if self.game.is_installed:
            UninstallGameDialog(game_id=self.game.id, parent=self.window)
        else:
            RemoveGameDialog(game_id=self.game.id, parent=self.window)

game property readonly

is_game_running property readonly

__init__(self, application=None, window=None) special

Source code in lutris/game_actions.py
def __init__(self, application=None, window=None):
    self.application = application or Gio.Application.get_default()
    self.window = window
    self.game_id = None
    self._game = None

get_displayed_entries(self)

Return a dictionary of actions that should be shown for a game

Source code in lutris/game_actions.py
def get_displayed_entries(self):
    """Return a dictionary of actions that should be shown for a game"""
    return {
        "add": not self.game.is_installed,
        "duplicate": True,
        "install": not self.game.is_installed,
        "play": self.game.is_installed and not self.is_game_running,
        "update": self.game.is_updatable,
        "install_dlcs": self.game.is_updatable,
        "stop": self.is_game_running,
        "configure": bool(self.game.is_installed),
        "browse": self.game.is_installed and self.game.runner_name != "browser",
        "show_logs": self.game.is_installed,
        "favorite": not self.game.is_favorite,
        "deletefavorite": self.game.is_favorite,
        "install_more": not self.game.service and self.game.is_installed,
        "execute-script": bool(
            self.game.is_installed and self.game.runner
            and self.game.runner.system_config.get("manual_command")
        ),
        "desktop-shortcut": (
            self.game.is_installed
            and not xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
        ),
        "menu-shortcut": (
            self.game.is_installed
            and not xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
        ),
        "rm-desktop-shortcut": bool(
            self.game.is_installed
            and xdgshortcuts.desktop_launcher_exists(self.game.slug, self.game.id)
        ),
        "rm-menu-shortcut": bool(
            self.game.is_installed
            and xdgshortcuts.menu_launcher_exists(self.game.slug, self.game.id)
        ),
        "remove": True,
        "view": True,
        "hide": self.game.is_installed and not self.game.is_hidden,
        "unhide": self.game.is_hidden,
    }

get_game_actions(self)

Return a list of game actions and their callbacks

Source code in lutris/game_actions.py
def get_game_actions(self):
    """Return a list of game actions and their callbacks"""
    return [
        ("play", _("Play"), self.on_game_launch),
        ("stop", _("Stop"), self.on_game_stop),
        ("install", _("Install"), self.on_install_clicked),
        ("update", _("Install updates"), self.on_update_clicked),
        ("install_dlcs", "Install DLCs", self.on_install_dlc_clicked),
        ("show_logs", _("Show logs"), self.on_show_logs),
        ("add", _("Add installed game"), self.on_add_manually),
        ("duplicate", _("Duplicate"), self.on_game_duplicate),
        ("configure", _("Configure"), self.on_edit_game_configuration),
        ("favorite", _("Add to favorites"), self.on_add_favorite_game),
        ("deletefavorite", _("Remove from favorites"), self.on_delete_favorite_game),
        ("execute-script", _("Execute script"), self.on_execute_script_clicked),
        ("browse", _("Browse files"), self.on_browse_files),
        (
            "desktop-shortcut",
            _("Create desktop shortcut"),
            self.on_create_desktop_shortcut,
        ),
        (
            "rm-desktop-shortcut",
            _("Delete desktop shortcut"),
            self.on_remove_desktop_shortcut,
        ),
        (
            "menu-shortcut",
            _("Create application menu shortcut"),
            self.on_create_menu_shortcut,
        ),
        (
            "rm-menu-shortcut",
            _("Delete application menu shortcut"),
            self.on_remove_menu_shortcut,
        ),
        ("install_more", _("Install another version"), self.on_install_clicked),
        ("remove", _("Remove"), self.on_remove_game),
        ("view", _("View on Lutris.net"), self.on_view_game),
        ("hide", _("Hide game from library"), self.on_hide_game),
        ("unhide", _("Unhide game from library"), self.on_unhide_game),
    ]

get_running_game(self)

Source code in lutris/game_actions.py
def get_running_game(self):
    ids = self.application.get_running_game_ids()
    for game_id in ids:
        if str(game_id) == str(self.game.id):
            return self.game
    logger.warning("Game %s not in %s", self.game_id, ids)

on_add_favorite_game(self, _widget)

Add to favorite Games list

Source code in lutris/game_actions.py
def on_add_favorite_game(self, _widget):
    """Add to favorite Games list"""
    self.game.add_to_favorites()

on_add_manually(self, _widget, *_args)

Callback that presents the Add game dialog

Source code in lutris/game_actions.py
def on_add_manually(self, _widget, *_args):
    """Callback that presents the Add game dialog"""
    return AddGameDialog(self.window, game=self.game, runner=self.game.runner_name)

on_browse_files(self, _widget)

Callback to open a game folder in the file browser

Source code in lutris/game_actions.py
def on_browse_files(self, _widget):
    """Callback to open a game folder in the file browser"""
    path = self.game.get_browse_dir()
    if not path:
        dialogs.NoticeDialog(_("This game has no installation directory"))
    elif path_exists(path):
        open_uri("file://%s" % path)
    else:
        dialogs.NoticeDialog(_("Can't open %s \nThe folder doesn't exist.") % path)

on_create_desktop_shortcut(self, *_args)

Create a desktop launcher for the selected game.

Source code in lutris/game_actions.py
def on_create_desktop_shortcut(self, *_args):
    """Create a desktop launcher for the selected game."""
    xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, desktop=True)

on_create_menu_shortcut(self, *_args)

Add the selected game to the system's Games menu.

Source code in lutris/game_actions.py
def on_create_menu_shortcut(self, *_args):
    """Add the selected game to the system's Games menu."""
    xdgshortcuts.create_launcher(self.game.slug, self.game.id, self.game.name, menu=True)

on_delete_favorite_game(self, _widget)

delete from favorites

Source code in lutris/game_actions.py
def on_delete_favorite_game(self, _widget):
    """delete from favorites"""
    self.game.remove_from_favorites()

on_edit_game_configuration(self, _widget)

Edit game preferences

Source code in lutris/game_actions.py
def on_edit_game_configuration(self, _widget):
    """Edit game preferences"""
    self.application.show_window(EditGameConfigDialog, game=self.game, parent=self.window)

on_execute_script_clicked(self, _widget)

Execute the game's associated script

Source code in lutris/game_actions.py
def on_execute_script_clicked(self, _widget):
    """Execute the game's associated script"""
    manual_command = self.game.runner.system_config.get("manual_command")
    if path_exists(manual_command):
        MonitoredCommand(
            [manual_command],
            include_processes=[os.path.basename(manual_command)],
            cwd=self.game.directory,
        ).start()
        logger.info("Running %s in the background", manual_command)

on_game_duplicate(self, _widget)

Source code in lutris/game_actions.py
def on_game_duplicate(self, _widget):
    confirm_dlg = QuestionDialog(
        {
            "parent": self.window,
            "question": _(
                "Do you wish to duplicate %s?\nThe configuration will be duplicated, "
                "but the games files will <b>not be duplicated</b>."
            ) % gtk_safe(self.game.name),
            "title": _("Duplicate game?"),
        }
    )
    if confirm_dlg.result != Gtk.ResponseType.YES:
        return

    assigned_name = get_unusued_game_name(self.game.name)
    old_config_id = self.game.game_config_id
    if old_config_id:
        new_config_id = duplicate_game_config(self.game.slug, old_config_id)
    else:
        new_config_id = None

    db_game = get_game_by_field(self.game.id, "id")
    db_game["name"] = assigned_name
    db_game["configpath"] = new_config_id
    db_game.pop("id")
    # Disconnect duplicate from service- there should be at most
    # 1 PGA game for a service game.
    db_game.pop("service", None)
    db_game.pop("service_id", None)

    game_id = add_game(**db_game)
    new_game = Game(game_id)
    new_game.save()

on_game_launch(self, *_args)

Launch a game

Source code in lutris/game_actions.py
def on_game_launch(self, *_args):
    """Launch a game"""
    self.game.launch()

on_game_stop(self, _caller)

Stops the game

Source code in lutris/game_actions.py
def on_game_stop(self, _caller):
    """Stops the game"""
    game = self.get_running_game()
    if game:
        game.force_stop()

on_hide_game(self, _widget)

Add a game to the list of hidden games

Source code in lutris/game_actions.py
def on_hide_game(self, _widget):
    """Add a game to the list of hidden games"""
    self.game.set_hidden(True)

on_install_clicked(self, *_args)

Install a game

Source code in lutris/game_actions.py
def on_install_clicked(self, *_args):
    """Install a game"""
    # Install the currently selected game in the UI
    if not self.game.slug:
        raise RuntimeError("No game to install: %s" % self.game.id)
    self.game.emit("game-install")

on_install_dlc_clicked(self, _widget)

Source code in lutris/game_actions.py
def on_install_dlc_clicked(self, _widget):
    self.game.emit("game-install-dlc")

on_locate_installed_game(self, _button, game)

Show the user a dialog to import an existing install to a DRM free service

Parameters:

Name Type Description Default
game Game

Game instance without a database ID, populated with a fields the service can provides

required
Source code in lutris/game_actions.py
def on_locate_installed_game(self, _button, game):
    """Show the user a dialog to import an existing install to a DRM free service

    Params:
        game (Game): Game instance without a database ID, populated with a fields the service can provides
    """
    AddGameDialog(self.window, game=game)

on_remove_desktop_shortcut(self, *_args)

Remove a .desktop shortcut

Source code in lutris/game_actions.py
def on_remove_desktop_shortcut(self, *_args):
    """Remove a .desktop shortcut"""
    xdgshortcuts.remove_launcher(self.game.slug, self.game.id, desktop=True)

on_remove_game(self, *_args)

Callback that present the uninstall dialog to the user

Source code in lutris/game_actions.py
def on_remove_game(self, *_args):
    """Callback that present the uninstall dialog to the user"""
    if self.game.is_installed:
        UninstallGameDialog(game_id=self.game.id, parent=self.window)
    else:
        RemoveGameDialog(game_id=self.game.id, parent=self.window)

on_remove_menu_shortcut(self, *_args)

Remove an XDG menu shortcut

Source code in lutris/game_actions.py
def on_remove_menu_shortcut(self, *_args):
    """Remove an XDG menu shortcut"""
    xdgshortcuts.remove_launcher(self.game.slug, self.game.id, menu=True)

on_show_logs(self, _widget)

Display game log

Source code in lutris/game_actions.py
def on_show_logs(self, _widget):
    """Display game log"""
    _buffer = self.game.log_buffer
    if not _buffer:
        logger.info("No log for game %s", self.game)
    return LogWindow(
        title=_("Log for {}").format(self.game),
        buffer=_buffer,
        application=self.application
    )

on_unhide_game(self, _widget)

Removes a game from the list of hidden games

Source code in lutris/game_actions.py
def on_unhide_game(self, _widget):
    """Removes a game from the list of hidden games"""
    self.game.set_hidden(False)

on_update_clicked(self, _widget)

Source code in lutris/game_actions.py
def on_update_clicked(self, _widget):
    self.game.emit("game-install-update")

on_view_game(self, _widget)

Callback to open a game on lutris.net

Source code in lutris/game_actions.py
def on_view_game(self, _widget):
    """Callback to open a game on lutris.net"""
    open_uri("https://lutris.net/games/%s" % self.game.slug)

set_game(self, game=None, game_id=None)

Source code in lutris/game_actions.py
def set_game(self, game=None, game_id=None):
    if game:
        self._game = game
        self.game_id = game.id
    else:
        self._game = None
        self.game_id = game_id

gui special

Lutris GUI package

addgameswindow

AddGamesWindow (BaseApplicationWindow)

Show a selection of ways to add games to Lutris

Source code in lutris/gui/addgameswindow.py
class AddGamesWindow(BaseApplicationWindow):  # pylint: disable=too-many-public-methods
    """Show a selection of ways to add games to Lutris"""

    sections = [
        (
            "system-search-symbolic",
            _("Search the Lutris website for installers"),
            _("Query our website for community installers"),
            "search_installers"
        ),
        (
            "folder-new-symbolic",
            _("Scan a folder for games"),
            _("Mass-import a folder of games"),
            "scan_folder"
        ),
        (
            "media-optical-dvd-symbolic",
            _("Install a Windows game from media"),
            _("Launch a setup file from an optical drive or download"),
            "install_from_setup"
        ),
        (
            "x-office-document-symbolic",
            _("Install from a local install script"),
            _("Run a YAML install script"),
            "install_from_script"
        ),
        (
            "list-add-symbolic",
            _("Add locally installed game"),
            _("Manually configure a game available locally"),
            "add_local_game"
        )
    ]

    title_text = _("Add games to Lutris")

    def __init__(self, application=None):
        super().__init__(application=application)
        self.set_default_size(640, 450)
        self.search_timer_id = None
        self.search_spinner = None
        self.text_query = None
        self.result_label = None
        self.title_label = Gtk.Label(visible=True)
        self.title_label.set_markup(f"<b>{self.title_text}</b>")
        self.vbox.pack_start(self.title_label, False, False, 12)

        self.listbox = Gtk.ListBox(visible=True)
        self.listbox.set_activate_on_single_click(True)
        self.vbox.pack_start(self.listbox, False, False, 12)
        for icon, text, subtext, callback_name in self.sections:
            row = self.build_row(icon, text, subtext)
            row.callback_name = callback_name

            self.listbox.add(row)
        self.listbox.connect("row-activated", self.on_row_activated)

    def on_row_activated(self, listbox, row):
        if row.callback_name:
            callback = getattr(self, row.callback_name)
            callback()

    def _get_row(self):
        row = Gtk.ListBoxRow(visible=True)
        row.set_selectable(False)
        row.set_activatable(True)
        return row

    def _get_box(self):
        return Gtk.Box(
            spacing=12,
            margin_right=12,
            margin_left=12,
            margin_top=12,
            margin_bottom=12,
            visible=True,
        )

    def _get_icon(self, name, small=False):
        if small:
            size = Gtk.IconSize.MENU
        else:
            size = Gtk.IconSize.DND
        icon = Gtk.Image.new_from_icon_name(name, size)
        icon.show()
        return icon

    def _get_label(self, text):
        label = Gtk.Label(visible=True)
        label.set_markup(text)
        label.set_alignment(0, 0.5)
        return label

    def build_row(self, icon_name, text, subtext):
        row = self._get_row()
        box = self._get_box()
        if icon_name:
            icon = self._get_icon(icon_name)
            box.pack_start(icon, False, False, 0)
        label = self._get_label(f"<b>{text}</b>\n{subtext}")
        box.pack_start(label, True, True, 0)
        if icon_name:
            next_icon = self._get_icon("go-next-symbolic", small=True)
            box.pack_start(next_icon, False, False, 0)
        row.add(box)
        return row

    def search_installers(self):
        """Search installers with the Lutris API"""
        self.title_label.set_markup("<b>Search Lutris.net</b>")
        self.listbox.destroy()
        hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, visible=True)
        entry = Gtk.SearchEntry(visible=True)
        hbox.pack_start(entry, True, True, 0)
        self.search_spinner = Gtk.Spinner(visible=False)
        hbox.pack_end(self.search_spinner, False, False, 6)
        self.vbox.add(hbox)
        self.result_label = self._get_label("")
        self.vbox.add(self.result_label)
        entry.connect("changed", self._on_search_updated)
        self.listbox = Gtk.ListBox()
        self.listbox.connect("row-activated", self._on_game_selected)
        scroll = Gtk.ScrolledWindow(visible=True)
        scroll.set_vexpand(True)
        scroll.add(self.listbox)
        self.vbox.add(scroll)
        entry.grab_focus()

    def scan_folder(self):
        """Scan a folder of already installed games"""
        self.title_label.set_markup("<b>Import games from a folder</b>")
        self.listbox.destroy()
        script_dlg = DirectoryDialog(_("Select folder to scan"))
        if not script_dlg.folder:
            self.destroy()
            return
        spinner = Gtk.Spinner(visible=True)
        spinner.start()
        self.vbox.pack_start(spinner, False, False, 18)
        AsyncCall(scan_directory, self._on_folder_scanned, script_dlg.folder)

    def _on_folder_scanned(self, result, error):
        if error:
            ErrorDialog(error)
            self.destroy()
            return
        for child in self.vbox.get_children():
            child.destroy()
        installed, missing = result
        installed_label = self._get_label("Installed games")
        self.vbox.add(installed_label)
        installed_listbox = Gtk.ListBox(visible=True)
        installed_scroll = Gtk.ScrolledWindow(visible=True)
        installed_scroll.set_vexpand(True)
        installed_scroll.add(installed_listbox)
        self.vbox.add(installed_scroll)
        for folder in installed:
            installed_listbox.add(self.build_row("", gtk_safe(folder), ""))

        missing_label = self._get_label("No match found")
        self.vbox.add(missing_label)
        missing_listbox = Gtk.ListBox(visible=True)
        missing_scroll = Gtk.ScrolledWindow(visible=True)
        missing_scroll.set_vexpand(True)
        missing_scroll.add(missing_listbox)
        self.vbox.add(missing_scroll)
        for folder in missing:
            missing_listbox.add(self.build_row("", gtk_safe(folder), ""))

    def _on_search_updated(self, entry):
        if self.search_timer_id:
            GLib.source_remove(self.search_timer_id)
        self.text_query = entry.get_text().strip()
        self.search_timer_id = GLib.timeout_add(750, self.update_search_results)

    def _on_game_selected(self, listbox, row):
        game_slug = row.api_info["slug"]
        installers = get_installers(game_slug=game_slug)
        application = Gio.Application.get_default()
        application.show_installer_window(installers)
        self.destroy()

    def update_search_results(self):
        # Don't start a search while another is going; defer it instead.
        if self.search_spinner.get_visible():
            self.search_timer_id = GLib.timeout_add(750, self.update_search_results)
            return

        self.search_timer_id = None

        if self.text_query:
            self.search_spinner.show()
            self.search_spinner.start()
            AsyncCall(api.search_games, self.update_search_results_cb, self.text_query)

    def update_search_results_cb(self, api_games, error):
        if error:
            ErrorDialog(error)
            return

        self.search_spinner.stop()
        self.search_spinner.hide()
        total_count = api_games.get("count", 0)
        count = len(api_games.get('results', []))

        if not count:
            self.result_label.set_markup(_("No results"))
        elif count == total_count:
            self.result_label.set_markup(_(f"Showing <b>{count}</b> results"))
        else:
            self.result_label.set_markup(_(f"<b>{total_count}</b> results, only displaying first {count}"))
        for row in self.listbox.get_children():
            row.destroy()
        for game in api_games.get("results", []):
            platforms = ",".join(gtk_safe(platform["name"]) for platform in game["platforms"])
            year = game['year'] or ""
            if platforms and year:
                platforms = ", " + platforms

            row = self.build_row("", gtk_safe(game['name']), f"{year}{platforms}")
            row.api_info = game
            self.listbox.add(row)
        self.listbox.show()

    def install_from_setup(self):
        """Install from a setup file"""
        self.title_label.set_markup(_("<b>Select setup file</b>"))
        self.listbox.destroy()
        label = self._get_label("Game name")
        self.vbox.add(label)
        entry = Gtk.Entry(visible=True)
        self.vbox.add(entry)
        button = Gtk.Button(_("Continue"), visible=True)
        button.connect("clicked", self._on_install_setup_continue, entry)
        self.vbox.add(button)

    def _on_install_setup_continue(self, button, entry):
        name = entry.get_text().strip()
        installer = {
            "name": name,
            "version": "Setup file",
            "slug": slugify(name) + "-setup",
            "game_slug": slugify(name),
            "runner": "wine",
            "script": {
                "game": {
                    "exe": AUTO_WIN32_EXE, "prefix": "$GAMEDIR"
                },
                "files": [
                    {"setupfile": "N/A:Select the setup file"}
                ],
                "installer": [
                    {"task": {"name": "wineexec", "executable": "setupfile"}}
                ]
            }
        }
        application = Gio.Application.get_default()
        application.show_installer_window([installer])
        self.destroy()

    def install_from_script(self):
        """Install from a YAML file"""
        script_dlg = FileDialog(_("Select a Lutris installer"))
        if script_dlg.filename:
            installers = get_installers(installer_file=script_dlg.filename)
            application = Gio.Application.get_default()
            application.show_installer_window(installers)
        self.destroy()

    def add_local_game(self):
        """Manually configure game"""
        AddGameDialog(None)
        self.destroy()
sections
title_text
__init__(self, application=None) special
Source code in lutris/gui/addgameswindow.py
def __init__(self, application=None):
    super().__init__(application=application)
    self.set_default_size(640, 450)
    self.search_timer_id = None
    self.search_spinner = None
    self.text_query = None
    self.result_label = None
    self.title_label = Gtk.Label(visible=True)
    self.title_label.set_markup(f"<b>{self.title_text}</b>")
    self.vbox.pack_start(self.title_label, False, False, 12)

    self.listbox = Gtk.ListBox(visible=True)
    self.listbox.set_activate_on_single_click(True)
    self.vbox.pack_start(self.listbox, False, False, 12)
    for icon, text, subtext, callback_name in self.sections:
        row = self.build_row(icon, text, subtext)
        row.callback_name = callback_name

        self.listbox.add(row)
    self.listbox.connect("row-activated", self.on_row_activated)
add_local_game(self)

Manually configure game

Source code in lutris/gui/addgameswindow.py
def add_local_game(self):
    """Manually configure game"""
    AddGameDialog(None)
    self.destroy()
build_row(self, icon_name, text, subtext)
Source code in lutris/gui/addgameswindow.py
def build_row(self, icon_name, text, subtext):
    row = self._get_row()
    box = self._get_box()
    if icon_name:
        icon = self._get_icon(icon_name)
        box.pack_start(icon, False, False, 0)
    label = self._get_label(f"<b>{text}</b>\n{subtext}")
    box.pack_start(label, True, True, 0)
    if icon_name:
        next_icon = self._get_icon("go-next-symbolic", small=True)
        box.pack_start(next_icon, False, False, 0)
    row.add(box)
    return row
install_from_script(self)

Install from a YAML file

Source code in lutris/gui/addgameswindow.py
def install_from_script(self):
    """Install from a YAML file"""
    script_dlg = FileDialog(_("Select a Lutris installer"))
    if script_dlg.filename:
        installers = get_installers(installer_file=script_dlg.filename)
        application = Gio.Application.get_default()
        application.show_installer_window(installers)
    self.destroy()
install_from_setup(self)

Install from a setup file

Source code in lutris/gui/addgameswindow.py
def install_from_setup(self):
    """Install from a setup file"""
    self.title_label.set_markup(_("<b>Select setup file</b>"))
    self.listbox.destroy()
    label = self._get_label("Game name")
    self.vbox.add(label)
    entry = Gtk.Entry(visible=True)
    self.vbox.add(entry)
    button = Gtk.Button(_("Continue"), visible=True)
    button.connect("clicked", self._on_install_setup_continue, entry)
    self.vbox.add(button)
on_row_activated(self, listbox, row)
Source code in lutris/gui/addgameswindow.py
def on_row_activated(self, listbox, row):
    if row.callback_name:
        callback = getattr(self, row.callback_name)
        callback()
scan_folder(self)

Scan a folder of already installed games

Source code in lutris/gui/addgameswindow.py
def scan_folder(self):
    """Scan a folder of already installed games"""
    self.title_label.set_markup("<b>Import games from a folder</b>")
    self.listbox.destroy()
    script_dlg = DirectoryDialog(_("Select folder to scan"))
    if not script_dlg.folder:
        self.destroy()
        return
    spinner = Gtk.Spinner(visible=True)
    spinner.start()
    self.vbox.pack_start(spinner, False, False, 18)
    AsyncCall(scan_directory, self._on_folder_scanned, script_dlg.folder)
search_installers(self)

Search installers with the Lutris API

Source code in lutris/gui/addgameswindow.py
def search_installers(self):
    """Search installers with the Lutris API"""
    self.title_label.set_markup("<b>Search Lutris.net</b>")
    self.listbox.destroy()
    hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, visible=True)
    entry = Gtk.SearchEntry(visible=True)
    hbox.pack_start(entry, True, True, 0)
    self.search_spinner = Gtk.Spinner(visible=False)
    hbox.pack_end(self.search_spinner, False, False, 6)
    self.vbox.add(hbox)
    self.result_label = self._get_label("")
    self.vbox.add(self.result_label)
    entry.connect("changed", self._on_search_updated)
    self.listbox = Gtk.ListBox()
    self.listbox.connect("row-activated", self._on_game_selected)
    scroll = Gtk.ScrolledWindow(visible=True)
    scroll.set_vexpand(True)
    scroll.add(self.listbox)
    self.vbox.add(scroll)
    entry.grab_focus()
update_search_results(self)
Source code in lutris/gui/addgameswindow.py
def update_search_results(self):
    # Don't start a search while another is going; defer it instead.
    if self.search_spinner.get_visible():
        self.search_timer_id = GLib.timeout_add(750, self.update_search_results)
        return

    self.search_timer_id = None

    if self.text_query:
        self.search_spinner.show()
        self.search_spinner.start()
        AsyncCall(api.search_games, self.update_search_results_cb, self.text_query)
update_search_results_cb(self, api_games, error)
Source code in lutris/gui/addgameswindow.py
def update_search_results_cb(self, api_games, error):
    if error:
        ErrorDialog(error)
        return

    self.search_spinner.stop()
    self.search_spinner.hide()
    total_count = api_games.get("count", 0)
    count = len(api_games.get('results', []))

    if not count:
        self.result_label.set_markup(_("No results"))
    elif count == total_count:
        self.result_label.set_markup(_(f"Showing <b>{count}</b> results"))
    else:
        self.result_label.set_markup(_(f"<b>{total_count}</b> results, only displaying first {count}"))
    for row in self.listbox.get_children():
        row.destroy()
    for game in api_games.get("results", []):
        platforms = ",".join(gtk_safe(platform["name"]) for platform in game["platforms"])
        year = game['year'] or ""
        if platforms and year:
            platforms = ", " + platforms

        row = self.build_row("", gtk_safe(game['name']), f"{year}{platforms}")
        row.api_info = game
        self.listbox.add(row)
    self.listbox.show()

application

Application (Application)

Source code in lutris/gui/application.py
class Application(Gtk.Application):

    def __init__(self):
        super().__init__(
            application_id="net.lutris.Lutris",
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
        )

        GObject.add_emission_hook(Game, "game-launch", self.on_game_launch)
        GObject.add_emission_hook(Game, "game-start", self.on_game_start)
        GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
        GObject.add_emission_hook(Game, "game-install", self.on_game_install)
        GObject.add_emission_hook(Game, "game-install-update", self.on_game_install_update)
        GObject.add_emission_hook(Game, "game-install-dlc", self.on_game_install_dlc)

        GLib.set_application_name(_("Lutris"))
        self.window = None

        self.running_games = Gio.ListStore.new(Game)
        self.app_windows = {}
        self.tray = None
        self.css_provider = Gtk.CssProvider.new()
        self.run_in_background = False
        self.style_manager = None

        if os.geteuid() == 0:
            ErrorDialog(_("Running Lutris as root is not recommended and may cause unexpected issues"))

        try:
            self.css_provider.load_from_path(os.path.join(datapath.get(), "ui", "lutris.css"))
        except GLib.Error as e:
            logger.exception(e)

        if hasattr(self, "add_main_option"):
            self.add_arguments()
        else:
            ErrorDialog(_("Your Linux distribution is too old. Lutris won't function properly."))

    def add_arguments(self):
        if hasattr(self, "set_option_context_summary"):
            self.set_option_context_summary(_(
                "Run a game directly by adding the parameter lutris:rungame/game-identifier.\n"
                "If several games share the same identifier you can use the numerical ID "
                "(displayed when running lutris --list-games) and add "
                "lutris:rungameid/numerical-id.\n"
                "To install a game, add lutris:install/game-identifier."
            ))
        else:
            logger.warning("GLib.set_option_context_summary missing, " "was added in GLib 2.56 (Released 2018-03-12)")
        self.add_main_option(
            "version",
            ord("v"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("Print the version of Lutris and exit"),
            None,
        )
        self.add_main_option(
            "debug",
            ord("d"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("Show debug messages"),
            None,
        )
        self.add_main_option(
            "install",
            ord("i"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Install a game from a yml file"),
            None,
        )
        self.add_main_option(
            "output-script",
            ord("b"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Generate a bash script to run a game without the client"),
            None,
        )
        self.add_main_option(
            "exec",
            ord("e"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Execute a program with the Lutris Runtime"),
            None,
        )
        self.add_main_option(
            "list-games",
            ord("l"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("List all games in database"),
            None,
        )
        self.add_main_option(
            "installed",
            ord("o"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("Only list installed games"),
            None,
        )
        self.add_main_option(
            "list-steam-games",
            ord("s"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("List available Steam games"),
            None,
        )
        self.add_main_option(
            "list-steam-folders",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("List all known Steam library folders"),
            None,
        )
        self.add_main_option(
            "list-runners",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("List all known runners"),
            None,
        )
        self.add_main_option(
            "list-wine-versions",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("List all known Wine versions"),
            None,
        )
        self.add_main_option(
            "install-runner",
            ord("r"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Install a Runner"),
            None,
        )
        self.add_main_option(
            "uninstall-runner",
            ord("u"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Uninstall a Runner"),
            None,
        )
        self.add_main_option(
            "export",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Export a game"),
            None,
        )
        self.add_main_option(
            "import",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Import a game"),
            None,
        )
        self.add_main_option(
            "dest",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Destination path for export"),
            None,
        )
        self.add_main_option(
            "json",
            ord("j"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("Display the list of games in JSON format"),
            None,
        )
        self.add_main_option(
            "reinstall",
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.NONE,
            _("Reinstall game"),
            None,
        )
        self.add_main_option("submit-issue", 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Submit an issue"), None)
        self.add_main_option(
            GLib.OPTION_REMAINING,
            0,
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING_ARRAY,
            _("URI to open"),
            "URI",
        )

    def do_startup(self):  # pylint: disable=arguments-differ
        """Sets up the application on first start."""
        Gtk.Application.do_startup(self)
        signal.signal(signal.SIGINT, signal.SIG_DFL)

        action = Gio.SimpleAction.new("quit")
        action.connect("activate", lambda *x: self.quit())
        self.add_action(action)
        self.add_accelerator("<Primary>q", "app.quit")

        self.style_manager = StyleManager()

    def do_activate(self):  # pylint: disable=arguments-differ
        Application.show_update_runtime_dialog()
        if not self.window:
            self.window = LutrisWindow(application=self)
            screen = self.window.props.screen  # pylint: disable=no-member
            Gtk.StyleContext.add_provider_for_screen(screen, self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
        if not self.run_in_background:
            self.window.present()
        else:
            # Reset run in background to False. Future calls will set it
            # accordingly
            self.run_in_background = False

    @staticmethod
    def show_update_runtime_dialog():
        if os.environ.get("LUTRIS_SKIP_INIT"):
            logger.debug("Skipping initialization")
        else:
            init_dialog = LutrisInitDialog(update_runtime)
            init_dialog.run()

    def get_window_key(self, **kwargs):
        if kwargs.get("appid"):
            return kwargs["appid"]
        if kwargs.get("runner"):
            return kwargs["runner"].name
        if kwargs.get("installers"):
            return kwargs["installers"][0]["game_slug"]
        if kwargs.get("game"):
            return str(kwargs["game"].id)
        return str(kwargs)

    def show_window(self, window_class, **kwargs):
        """Instanciate a window keeping 1 instance max

        Params:
            window_class (Gtk.Window): class to create the instance from
            kwargs (dict): Additional arguments to pass to the instanciated window

        Returns:
            Gtk.Window: the existing window instance or a newly created one
        """
        window_key = str(window_class.__name__) + self.get_window_key(**kwargs)
        if self.app_windows.get(window_key):
            self.app_windows[window_key].present()
            return self.app_windows[window_key]
        if issubclass(window_class, Gtk.Dialog):
            if "parent" in kwargs:
                window_inst = window_class(**kwargs)
            else:
                window_inst = window_class(parent=self.window, **kwargs)
            window_inst.set_application(self)
        else:
            window_inst = window_class(application=self, **kwargs)
        window_inst.connect("destroy", self.on_app_window_destroyed, self.get_window_key(**kwargs))
        self.app_windows[window_key] = window_inst
        logger.debug("Showing window %s", window_key)
        window_inst.show()
        return window_inst

    def show_installer_window(self, installers, service=None, appid=None, is_update=False):
        self.show_window(
            InstallerWindow,
            installers=installers,
            service=service,
            appid=appid,
            is_update=is_update
        )

    def on_app_window_destroyed(self, app_window, window_key):
        """Remove the reference to the window when it has been destroyed"""
        window_key = str(app_window.__class__.__name__) + window_key
        try:
            del self.app_windows[window_key]
            logger.debug("Removed window %s", window_key)
        except KeyError:
            logger.warning("Failed to remove window %s", window_key)
            logger.info("Available windows: %s", ", ".join(self.app_windows.keys()))
        return True

    @staticmethod
    def _print(command_line, string):
        # Workaround broken pygobject bindings
        command_line.do_print_literal(command_line, string + "\n")

    def generate_script(self, db_game, script_path):
        """Output a script to a file.
        The script is capable of launching a game without the client
        """
        game = Game(db_game["id"])
        game.load_config()
        game.write_script(script_path)

    def do_command_line(self, command_line):  # noqa: C901  # pylint: disable=arguments-differ
        # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
        # pylint: disable=too-many-statements
        # TODO: split into multiple methods to reduce complexity (35)
        options = command_line.get_options_dict()

        # Use stdout to output logs, only if no command line argument is
        # provided
        argc = len(sys.argv) - 1
        if "-d" in sys.argv or "--debug" in sys.argv:
            argc -= 1
        if not argc:
            # Switch back the log output to stderr (the default in Python)
            # to avoid messing with any output from command line options.

            # Use when targetting Python 3.7 minimum
            # console_handler.setStream(sys.stderr)

            # Until then...
            logger.removeHandler(log.console_handler)
            log.console_handler = logging.StreamHandler(stream=sys.stdout)
            log.console_handler.setFormatter(log.SIMPLE_FORMATTER)
            logger.addHandler(log.console_handler)

        # Set up logger
        if options.contains("debug"):
            log.console_handler.setFormatter(log.DEBUG_FORMATTER)
            logger.setLevel(logging.DEBUG)

        # Text only commands

        # Print Lutris version and exit
        if options.contains("version"):
            executable_name = os.path.basename(sys.argv[0])
            print(executable_name + "-" + settings.VERSION)
            logger.setLevel(logging.NOTSET)
            return 0

        init_lutris()
        migrate()
        run_all_checks()

        if options.contains("dest"):
            dest_dir = options.lookup_value("dest").get_string()
        else:
            dest_dir = None

        # List game
        if options.contains("list-games"):
            game_list = games_db.get_games()
            if options.contains("installed"):
                game_list = [game for game in game_list if game["installed"]]
            if options.contains("json"):
                self.print_game_json(command_line, game_list)
            else:
                self.print_game_list(command_line, game_list)
            return 0

        # List Steam games
        if options.contains("list-steam-games"):
            self.print_steam_list(command_line)
            return 0

        # List Steam folders
        if options.contains("list-steam-folders"):
            self.print_steam_folders(command_line)
            return 0

        # List Runners
        if options.contains("list-runners"):
            self.print_runners()
            return 0

        # List Wine Runners
        if options.contains("list-wine-runners"):
            self.print_wine_runners()
            return 0

        # install Runner
        if options.contains("install-runner"):
            runner = options.lookup_value("install-runner").get_string()
            self.install_runner(runner)
            return 0

        # Uninstall Runner
        if options.contains("uninstall-runner"):
            runner = options.lookup_value("uninstall-runner").get_string()
            self.uninstall_runner(runner)
            return 0

        if options.contains("export"):
            slug = options.lookup_value("export").get_string()
            if not dest_dir:
                print("No destination dir given")
            else:
                export_game(slug, dest_dir)
            return 0

        if options.contains("import"):
            filepath = options.lookup_value("import").get_string()
            if not dest_dir:
                print("No destination dir given")
            else:
                import_game(filepath, dest_dir)
            return 0

        # Execute command in Lutris context
        if options.contains("exec"):
            command = options.lookup_value("exec").get_string()
            self.execute_command(command)
            return 0

        if options.contains("submit-issue"):
            IssueReportWindow(application=self)
            return 0

        try:
            url = options.lookup_value(GLib.OPTION_REMAINING)
            installer_info = self.get_lutris_action(url)
        except ValueError:
            self._print(command_line, _("%s is not a valid URI") % url.get_strv())
            return 1

        game_slug = installer_info["game_slug"]
        action = installer_info["action"]
        service = installer_info["service"]
        appid = installer_info["appid"]

        if options.contains("output-script"):
            action = "write-script"

        revision = installer_info["revision"]

        installer_file = None
        if options.contains("install"):
            installer_file = options.lookup_value("install").get_string()
            if installer_file.startswith(("http:", "https:")):
                try:
                    request = Request(installer_file).get()
                except HTTPError:
                    self._print(command_line, _("Failed to download %s") % installer_file)
                    return 1
                try:
                    headers = dict(request.response_headers)
                    file_name = headers["Content-Disposition"].split("=", 1)[-1]
                except (KeyError, IndexError):
                    file_name = os.path.basename(installer_file)
                file_path = os.path.join(tempfile.gettempdir(), file_name)
                self._print(command_line, _("download {url} to {file} started").format(
                    url=installer_file, file=file_path))
                with open(file_path, 'wb') as dest_file:
                    dest_file.write(request.content)
                installer_file = file_path
                action = "install"
            else:
                installer_file = os.path.abspath(installer_file)
                action = "install"

            if not os.path.isfile(installer_file):
                self._print(command_line, _("No such file: %s") % installer_file)
                return 1

        db_game = None
        if game_slug and not service:
            if action == "rungameid":
                # Force db_game to use game id
                self.run_in_background = True
                db_game = games_db.get_game_by_field(game_slug, "id")
            elif action == "rungame":
                # Force db_game to use game slug
                self.run_in_background = True
                db_game = games_db.get_game_by_field(game_slug, "slug")
            elif action == "install":
                # Installers can use game or installer slugs
                self.run_in_background = True
                db_game = games_db.get_game_by_field(game_slug, "slug") \
                    or games_db.get_game_by_field(game_slug, "installer_slug")
            else:
                # Dazed and confused, try anything that might works
                db_game = (
                    games_db.get_game_by_field(game_slug, "id")
                    or games_db.get_game_by_field(game_slug, "slug")
                    or games_db.get_game_by_field(game_slug, "installer_slug")
                )

        # If reinstall flag is passed, force the action to install
        if options.contains("reinstall"):
            action = "install"

        if action == "write-script":
            if not db_game or not db_game["id"]:
                logger.warning("No game provided to generate the script")
                return 1
            self.generate_script(db_game, options.lookup_value("output-script").get_string())
            return 0

        # Graphical commands
        self.activate()
        self.set_tray_icon()

        if not action:
            if db_game and db_game["installed"]:
                # Game found but no action provided, ask what to do
                dlg = InstallOrPlayDialog(db_game["name"])
                if not dlg.action_confirmed:
                    action = None
                elif dlg.action == "play":
                    action = "rungame"
                elif dlg.action == "install":
                    action = "install"
            elif game_slug or installer_file or service:
                # No game found, default to install if a game_slug or
                # installer_file is provided
                action = "install"

        if service:
            service_game = ServiceGameCollection.get_game(service, appid)
            if service_game:
                service = get_enabled_services()[service]()
                service.install(service_game)
                return 0

        if action == "install":
            installers = get_installers(
                game_slug=game_slug,
                installer_file=installer_file,
                revision=revision,
            )
            if installers:
                self.show_installer_window(installers)

        elif action in ("rungame", "rungameid"):
            if not db_game or not db_game["id"]:
                logger.warning("No game found in library")
                if not self.window.is_visible():
                    self.do_shutdown()
                return 0
            game = Game(db_game["id"])
            self.on_game_launch(game)
        return 0

    def on_game_launch(self, game):
        game.launch()
        return True  # Return True to continue handling the emission hook

    def on_game_start(self, game):
        self.running_games.append(game)
        if settings.read_setting("hide_client_on_game_start") == "True":
            self.window.hide()  # Hide launcher window
        return True

    def on_game_install(self, game):
        """Request installation of a game"""
        if game.service and game.service != "lutris":
            service = get_enabled_services()[game.service]()
            db_game = ServiceGameCollection.get_game(service.id, game.appid)
            if not db_game:
                logger.error("Can't find %s for %s", game.name, service.name)
                return True
            try:
                game_id = service.install(db_game)
            except ValueError as e:
                logger.debug(e)
                game_id = None

            if game_id:
                game = Game(game_id)
                game.launch()
            return True
        if not game.slug:
            raise ValueError("Invalid game passed: %s" % game)
            # return True
        installers = get_installers(game_slug=game.slug)
        if installers:
            self.show_installer_window(installers)
        else:
            ErrorDialog(_("There is no installer available for %s.") % game.name, parent=self.window)
        return True

    def on_game_install_update(self, game):
        service = get_enabled_services()[game.service]()
        db_game = games_db.get_game_by_field(game.id, "id")
        installers = service.get_update_installers(db_game)
        if installers:
            self.show_installer_window(installers, service, game.appid, is_update=True)
        else:
            ErrorDialog(_("No updates found"))
        return True

    def on_game_install_dlc(self, game):
        service = get_enabled_services()[game.service]()
        db_game = games_db.get_game_by_field(game.id, "id")
        installers = service.get_dlc_installers(db_game)
        if installers:
            self.show_installer_window(installers, service, game.appid)
        else:
            ErrorDialog(_("No DLC found"))
        return True

    def get_running_game_ids(self):
        ids = []
        for i in range(self.running_games.get_n_items()):
            game = self.running_games.get_item(i)
            ids.append(str(game.id))
        return ids

    def get_game_by_id(self, game_id):
        for i in range(self.running_games.get_n_items()):
            game = self.running_games.get_item(i)
            if str(game.id) == str(game_id):
                return game
        return None

    def on_game_stop(self, game):
        """Callback to remove the game from the running games"""
        ids = self.get_running_game_ids()
        if str(game.id) in ids:
            try:
                self.running_games.remove(ids.index(str(game.id)))
            except ValueError:
                pass
        else:
            logger.warning("%s not in %s", game.id, ids)

        game.emit("game-stopped")
        if settings.read_setting("hide_client_on_game_start") == "True":
            self.window.show()  # Show launcher window
        elif not self.window.is_visible():
            if self.running_games.get_n_items() == 0:
                self.quit()
        return True

    @staticmethod
    def get_lutris_action(url):
        installer_info = {"game_slug": None, "revision": None, "action": None, "service": None, "appid": None}

        if url:
            url = url.get_strv()

        if url:
            url = url[0]
            installer_info = parse_installer_url(url)
            if installer_info is False:
                raise ValueError
        return installer_info

    def print_game_list(self, command_line, game_list):
        for game in game_list:
            self._print(
                command_line,
                "{:4} | {:<40} | {:<40} | {:<15} | {:<64}".format(
                    game["id"],
                    game["name"][:40],
                    game["slug"][:40],
                    game["runner"] or "-",
                    game["directory"] or "-",
                ),
            )

    def print_game_json(self, command_line, game_list):
        games = [
            {
                "id": game["id"],
                "slug": game["slug"],
                "name": game["name"],
                "runner": game["runner"],
                "platform": game["platform"] or None,
                "year": game["year"] or None,
                "directory": game["directory"] or None,
                "hidden": bool(game["hidden"]),
                "playtime": (
                    str(timedelta(hours=game["playtime"]))
                    if game["playtime"] else None
                ),
                "lastplayed": (
                    str(datetime.fromtimestamp(game["lastplayed"]))
                    if game["lastplayed"] else None
                )
            } for game in game_list
        ]
        self._print(command_line, json.dumps(games, indent=2))

    def print_steam_list(self, command_line):
        steamapps_paths = get_steamapps_paths()
        for path in steamapps_paths if steamapps_paths else []:
            appmanifest_files = get_appmanifests(path)
            for appmanifest_file in appmanifest_files:
                appmanifest = AppManifest(os.path.join(path, appmanifest_file))
                self._print(
                    command_line,
                    " {:8} | {:<60} | {}".format(
                        appmanifest.steamid,
                        appmanifest.name or "-",
                        ", ".join(appmanifest.states),
                    ),
                )

    @staticmethod
    def execute_command(command):
        """Execute an arbitrary command in a Lutris context
        with the runtime enabled and monitored by a MonitoredCommand
        """
        Application.show_update_runtime_dialog()
        logger.info("Running command '%s'", command)
        monitored_command = exec_command(command)
        try:
            GLib.MainLoop().run()
        except KeyboardInterrupt:
            monitored_command.stop()

    def print_steam_folders(self, command_line):
        steamapps_paths = get_steamapps_paths()
        for platform in ("linux", "windows"):
            for path in steamapps_paths[platform] if steamapps_paths else []:
                self._print(command_line, path)

    def print_runners(self):
        runnersName = get_runner_names()
        sortednames = sorted(runnersName.keys(), key=lambda x: x.lower())
        for name in sortednames:
            print(name)

    def print_wine_runners(self):
        runnersName = get_runners("wine")
        for i in runnersName["versions"]:
            if i["version"]:
                print(i)

    def install_runner(self, runner):
        if runner.startswith("lutris"):
            self.install_wine_cli(runner)
        else:
            self.install_cli(runner)

    def uninstall_runner(self, runner):
        if "wine" in runner:
            print("Are sure you want to delete Wine and all of the installed runners?[Y/N]")
            ans = input()
            if ans.lower() in ("y", "yes"):
                self.uninstall_runner_cli(runner)
            else:
                print("Not Removing Wine")
        elif runner.startswith("lutris"):
            self.wine_runner_uninstall(runner)
        else:
            self.uninstall_runner_cli(runner)

    def install_wine_cli(self, version):
        """
        Downloads wine runner using lutris -r <runner>
        """

        WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
        runner_path = os.path.join(WINE_DIR, f"{version}{'' if '-x86_64' in version else '-x86_64'}")
        if os.path.isdir(runner_path):
            print(f"Wine version '{version}' is already installed.")
        else:

            try:
                runner = import_runner("wine")
                runner().install(downloader=simple_downloader, version=version)
                print(f"Wine version '{version}' has been installed.")
            except (InvalidRunner, RunnerInstallationError) as ex:
                print(ex.message)

    def wine_runner_uninstall(self, version):
        version = f"{version}{'' if '-x86_64' in version else '-x86_64'}"
        WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
        runner_path = os.path.join(WINE_DIR, version)
        if os.path.isdir(runner_path):
            system.remove_folder(runner_path)
            print(f"Wine version '{version}' has been removed.")
        else:
            print(f"""
Specified version of Wine is not installed: {version}.
Please check if the Wine Runner and specified version are installed (for that use --list-wine-runners).
Also, check that the version specified is in the correct format.
                """)

    def install_cli(self, runner_name):
        """
        install the runner provided in prepare_runner_cli()
        """

        runner_path = os.path.join(settings.RUNNER_DIR, runner_name)
        if os.path.isdir(runner_path):
            print(f"'{runner_name}' is already installed.")
        else:
            try:
                runner = import_runner(runner_name)
                runner().install(version=None, downloader=simple_downloader, callback=None)
                print(f"'{runner_name}' has been installed")
            except (InvalidRunner, RunnerInstallationError) as ex:
                print(ex.message)

    def uninstall_runner_cli(self, runner_name):
        """
        uninstall the runner given in application file located in lutris/gui/application.py
        provided using lutris -u <runner>
        """
        try:
            runner_class = import_runner(runner_name)
            runner = runner_class()
        except InvalidRunner:
            logger.error("Failed to import Runner: %s", runner_name)
            return
        if not runner.is_installed():
            print(f"Runner '{runner_name}' is not installed.")
            return
        if runner.can_uninstall():
            runner.uninstall()
            print(f"'{runner_name}' has been uninstalled.")
        else:
            print(f"Runner '{runner_name}' cannot be uninstalled.")

    def do_shutdown(self):  # pylint: disable=arguments-differ
        logger.info("Shutting down Lutris")
        if self.window:
            settings.write_setting("selected_category", self.window.selected_category)
            self.window.destroy()
        Gtk.Application.do_shutdown(self)

    def set_tray_icon(self):
        """Creates or destroys a tray icon for the application"""
        active = settings.read_setting("show_tray_icon", default="false").lower() == "true"
        if active and not self.tray:
            self.tray = LutrisStatusIcon(application=self)
        if self.tray:
            self.tray.set_visible(active)
__init__(self) special
Source code in lutris/gui/application.py
def __init__(self):
    super().__init__(
        application_id="net.lutris.Lutris",
        flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
    )

    GObject.add_emission_hook(Game, "game-launch", self.on_game_launch)
    GObject.add_emission_hook(Game, "game-start", self.on_game_start)
    GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
    GObject.add_emission_hook(Game, "game-install", self.on_game_install)
    GObject.add_emission_hook(Game, "game-install-update", self.on_game_install_update)
    GObject.add_emission_hook(Game, "game-install-dlc", self.on_game_install_dlc)

    GLib.set_application_name(_("Lutris"))
    self.window = None

    self.running_games = Gio.ListStore.new(Game)
    self.app_windows = {}
    self.tray = None
    self.css_provider = Gtk.CssProvider.new()
    self.run_in_background = False
    self.style_manager = None

    if os.geteuid() == 0:
        ErrorDialog(_("Running Lutris as root is not recommended and may cause unexpected issues"))

    try:
        self.css_provider.load_from_path(os.path.join(datapath.get(), "ui", "lutris.css"))
    except GLib.Error as e:
        logger.exception(e)

    if hasattr(self, "add_main_option"):
        self.add_arguments()
    else:
        ErrorDialog(_("Your Linux distribution is too old. Lutris won't function properly."))
add_arguments(self)
Source code in lutris/gui/application.py
def add_arguments(self):
    if hasattr(self, "set_option_context_summary"):
        self.set_option_context_summary(_(
            "Run a game directly by adding the parameter lutris:rungame/game-identifier.\n"
            "If several games share the same identifier you can use the numerical ID "
            "(displayed when running lutris --list-games) and add "
            "lutris:rungameid/numerical-id.\n"
            "To install a game, add lutris:install/game-identifier."
        ))
    else:
        logger.warning("GLib.set_option_context_summary missing, " "was added in GLib 2.56 (Released 2018-03-12)")
    self.add_main_option(
        "version",
        ord("v"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("Print the version of Lutris and exit"),
        None,
    )
    self.add_main_option(
        "debug",
        ord("d"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("Show debug messages"),
        None,
    )
    self.add_main_option(
        "install",
        ord("i"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Install a game from a yml file"),
        None,
    )
    self.add_main_option(
        "output-script",
        ord("b"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Generate a bash script to run a game without the client"),
        None,
    )
    self.add_main_option(
        "exec",
        ord("e"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Execute a program with the Lutris Runtime"),
        None,
    )
    self.add_main_option(
        "list-games",
        ord("l"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("List all games in database"),
        None,
    )
    self.add_main_option(
        "installed",
        ord("o"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("Only list installed games"),
        None,
    )
    self.add_main_option(
        "list-steam-games",
        ord("s"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("List available Steam games"),
        None,
    )
    self.add_main_option(
        "list-steam-folders",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("List all known Steam library folders"),
        None,
    )
    self.add_main_option(
        "list-runners",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("List all known runners"),
        None,
    )
    self.add_main_option(
        "list-wine-versions",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("List all known Wine versions"),
        None,
    )
    self.add_main_option(
        "install-runner",
        ord("r"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Install a Runner"),
        None,
    )
    self.add_main_option(
        "uninstall-runner",
        ord("u"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Uninstall a Runner"),
        None,
    )
    self.add_main_option(
        "export",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Export a game"),
        None,
    )
    self.add_main_option(
        "import",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Import a game"),
        None,
    )
    self.add_main_option(
        "dest",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING,
        _("Destination path for export"),
        None,
    )
    self.add_main_option(
        "json",
        ord("j"),
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("Display the list of games in JSON format"),
        None,
    )
    self.add_main_option(
        "reinstall",
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.NONE,
        _("Reinstall game"),
        None,
    )
    self.add_main_option("submit-issue", 0, GLib.OptionFlags.NONE, GLib.OptionArg.NONE, _("Submit an issue"), None)
    self.add_main_option(
        GLib.OPTION_REMAINING,
        0,
        GLib.OptionFlags.NONE,
        GLib.OptionArg.STRING_ARRAY,
        _("URI to open"),
        "URI",
    )
do_activate(self)

activate(self)

Source code in lutris/gui/application.py
def do_activate(self):  # pylint: disable=arguments-differ
    Application.show_update_runtime_dialog()
    if not self.window:
        self.window = LutrisWindow(application=self)
        screen = self.window.props.screen  # pylint: disable=no-member
        Gtk.StyleContext.add_provider_for_screen(screen, self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
    if not self.run_in_background:
        self.window.present()
    else:
        # Reset run in background to False. Future calls will set it
        # accordingly
        self.run_in_background = False
do_command_line(self, command_line)

command_line(self, command_line:Gio.ApplicationCommandLine) -> int

Source code in lutris/gui/application.py
def do_command_line(self, command_line):  # noqa: C901  # pylint: disable=arguments-differ
    # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
    # pylint: disable=too-many-statements
    # TODO: split into multiple methods to reduce complexity (35)
    options = command_line.get_options_dict()

    # Use stdout to output logs, only if no command line argument is
    # provided
    argc = len(sys.argv) - 1
    if "-d" in sys.argv or "--debug" in sys.argv:
        argc -= 1
    if not argc:
        # Switch back the log output to stderr (the default in Python)
        # to avoid messing with any output from command line options.

        # Use when targetting Python 3.7 minimum
        # console_handler.setStream(sys.stderr)

        # Until then...
        logger.removeHandler(log.console_handler)
        log.console_handler = logging.StreamHandler(stream=sys.stdout)
        log.console_handler.setFormatter(log.SIMPLE_FORMATTER)
        logger.addHandler(log.console_handler)

    # Set up logger
    if options.contains("debug"):
        log.console_handler.setFormatter(log.DEBUG_FORMATTER)
        logger.setLevel(logging.DEBUG)

    # Text only commands

    # Print Lutris version and exit
    if options.contains("version"):
        executable_name = os.path.basename(sys.argv[0])
        print(executable_name + "-" + settings.VERSION)
        logger.setLevel(logging.NOTSET)
        return 0

    init_lutris()
    migrate()
    run_all_checks()

    if options.contains("dest"):
        dest_dir = options.lookup_value("dest").get_string()
    else:
        dest_dir = None

    # List game
    if options.contains("list-games"):
        game_list = games_db.get_games()
        if options.contains("installed"):
            game_list = [game for game in game_list if game["installed"]]
        if options.contains("json"):
            self.print_game_json(command_line, game_list)
        else:
            self.print_game_list(command_line, game_list)
        return 0

    # List Steam games
    if options.contains("list-steam-games"):
        self.print_steam_list(command_line)
        return 0

    # List Steam folders
    if options.contains("list-steam-folders"):
        self.print_steam_folders(command_line)
        return 0

    # List Runners
    if options.contains("list-runners"):
        self.print_runners()
        return 0

    # List Wine Runners
    if options.contains("list-wine-runners"):
        self.print_wine_runners()
        return 0

    # install Runner
    if options.contains("install-runner"):
        runner = options.lookup_value("install-runner").get_string()
        self.install_runner(runner)
        return 0

    # Uninstall Runner
    if options.contains("uninstall-runner"):
        runner = options.lookup_value("uninstall-runner").get_string()
        self.uninstall_runner(runner)
        return 0

    if options.contains("export"):
        slug = options.lookup_value("export").get_string()
        if not dest_dir:
            print("No destination dir given")
        else:
            export_game(slug, dest_dir)
        return 0

    if options.contains("import"):
        filepath = options.lookup_value("import").get_string()
        if not dest_dir:
            print("No destination dir given")
        else:
            import_game(filepath, dest_dir)
        return 0

    # Execute command in Lutris context
    if options.contains("exec"):
        command = options.lookup_value("exec").get_string()
        self.execute_command(command)
        return 0

    if options.contains("submit-issue"):
        IssueReportWindow(application=self)
        return 0

    try:
        url = options.lookup_value(GLib.OPTION_REMAINING)
        installer_info = self.get_lutris_action(url)
    except ValueError:
        self._print(command_line, _("%s is not a valid URI") % url.get_strv())
        return 1

    game_slug = installer_info["game_slug"]
    action = installer_info["action"]
    service = installer_info["service"]
    appid = installer_info["appid"]

    if options.contains("output-script"):
        action = "write-script"

    revision = installer_info["revision"]

    installer_file = None
    if options.contains("install"):
        installer_file = options.lookup_value("install").get_string()
        if installer_file.startswith(("http:", "https:")):
            try:
                request = Request(installer_file).get()
            except HTTPError:
                self._print(command_line, _("Failed to download %s") % installer_file)
                return 1
            try:
                headers = dict(request.response_headers)
                file_name = headers["Content-Disposition"].split("=", 1)[-1]
            except (KeyError, IndexError):
                file_name = os.path.basename(installer_file)
            file_path = os.path.join(tempfile.gettempdir(), file_name)
            self._print(command_line, _("download {url} to {file} started").format(
                url=installer_file, file=file_path))
            with open(file_path, 'wb') as dest_file:
                dest_file.write(request.content)
            installer_file = file_path
            action = "install"
        else:
            installer_file = os.path.abspath(installer_file)
            action = "install"

        if not os.path.isfile(installer_file):
            self._print(command_line, _("No such file: %s") % installer_file)
            return 1

    db_game = None
    if game_slug and not service:
        if action == "rungameid":
            # Force db_game to use game id
            self.run_in_background = True
            db_game = games_db.get_game_by_field(game_slug, "id")
        elif action == "rungame":
            # Force db_game to use game slug
            self.run_in_background = True
            db_game = games_db.get_game_by_field(game_slug, "slug")
        elif action == "install":
            # Installers can use game or installer slugs
            self.run_in_background = True
            db_game = games_db.get_game_by_field(game_slug, "slug") \
                or games_db.get_game_by_field(game_slug, "installer_slug")
        else:
            # Dazed and confused, try anything that might works
            db_game = (
                games_db.get_game_by_field(game_slug, "id")
                or games_db.get_game_by_field(game_slug, "slug")
                or games_db.get_game_by_field(game_slug, "installer_slug")
            )

    # If reinstall flag is passed, force the action to install
    if options.contains("reinstall"):
        action = "install"

    if action == "write-script":
        if not db_game or not db_game["id"]:
            logger.warning("No game provided to generate the script")
            return 1
        self.generate_script(db_game, options.lookup_value("output-script").get_string())
        return 0

    # Graphical commands
    self.activate()
    self.set_tray_icon()

    if not action:
        if db_game and db_game["installed"]:
            # Game found but no action provided, ask what to do
            dlg = InstallOrPlayDialog(db_game["name"])
            if not dlg.action_confirmed:
                action = None
            elif dlg.action == "play":
                action = "rungame"
            elif dlg.action == "install":
                action = "install"
        elif game_slug or installer_file or service:
            # No game found, default to install if a game_slug or
            # installer_file is provided
            action = "install"

    if service:
        service_game = ServiceGameCollection.get_game(service, appid)
        if service_game:
            service = get_enabled_services()[service]()
            service.install(service_game)
            return 0

    if action == "install":
        installers = get_installers(
            game_slug=game_slug,
            installer_file=installer_file,
            revision=revision,
        )
        if installers:
            self.show_installer_window(installers)

    elif action in ("rungame", "rungameid"):
        if not db_game or not db_game["id"]:
            logger.warning("No game found in library")
            if not self.window.is_visible():
                self.do_shutdown()
            return 0
        game = Game(db_game["id"])
        self.on_game_launch(game)
    return 0
do_shutdown(self)

shutdown(self)

Source code in lutris/gui/application.py
def do_shutdown(self):  # pylint: disable=arguments-differ
    logger.info("Shutting down Lutris")
    if self.window:
        settings.write_setting("selected_category", self.window.selected_category)
        self.window.destroy()
    Gtk.Application.do_shutdown(self)
do_startup(self)

Sets up the application on first start.

Source code in lutris/gui/application.py
def do_startup(self):  # pylint: disable=arguments-differ
    """Sets up the application on first start."""
    Gtk.Application.do_startup(self)
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    action = Gio.SimpleAction.new("quit")
    action.connect("activate", lambda *x: self.quit())
    self.add_action(action)
    self.add_accelerator("<Primary>q", "app.quit")

    self.style_manager = StyleManager()
execute_command(command) staticmethod

Execute an arbitrary command in a Lutris context with the runtime enabled and monitored by a MonitoredCommand

Source code in lutris/gui/application.py
@staticmethod
def execute_command(command):
    """Execute an arbitrary command in a Lutris context
    with the runtime enabled and monitored by a MonitoredCommand
    """
    Application.show_update_runtime_dialog()
    logger.info("Running command '%s'", command)
    monitored_command = exec_command(command)
    try:
        GLib.MainLoop().run()
    except KeyboardInterrupt:
        monitored_command.stop()
generate_script(self, db_game, script_path)

Output a script to a file. The script is capable of launching a game without the client

Source code in lutris/gui/application.py
def generate_script(self, db_game, script_path):
    """Output a script to a file.
    The script is capable of launching a game without the client
    """
    game = Game(db_game["id"])
    game.load_config()
    game.write_script(script_path)
get_game_by_id(self, game_id)
Source code in lutris/gui/application.py
def get_game_by_id(self, game_id):
    for i in range(self.running_games.get_n_items()):
        game = self.running_games.get_item(i)
        if str(game.id) == str(game_id):
            return game
    return None
get_lutris_action(url) staticmethod
Source code in lutris/gui/application.py
@staticmethod
def get_lutris_action(url):
    installer_info = {"game_slug": None, "revision": None, "action": None, "service": None, "appid": None}

    if url:
        url = url.get_strv()

    if url:
        url = url[0]
        installer_info = parse_installer_url(url)
        if installer_info is False:
            raise ValueError
    return installer_info
get_running_game_ids(self)
Source code in lutris/gui/application.py
def get_running_game_ids(self):
    ids = []
    for i in range(self.running_games.get_n_items()):
        game = self.running_games.get_item(i)
        ids.append(str(game.id))
    return ids
get_window_key(self, **kwargs)
Source code in lutris/gui/application.py
def get_window_key(self, **kwargs):
    if kwargs.get("appid"):
        return kwargs["appid"]
    if kwargs.get("runner"):
        return kwargs["runner"].name
    if kwargs.get("installers"):
        return kwargs["installers"][0]["game_slug"]
    if kwargs.get("game"):
        return str(kwargs["game"].id)
    return str(kwargs)
install_cli(self, runner_name)

install the runner provided in prepare_runner_cli()

Source code in lutris/gui/application.py
def install_cli(self, runner_name):
    """
    install the runner provided in prepare_runner_cli()
    """

    runner_path = os.path.join(settings.RUNNER_DIR, runner_name)
    if os.path.isdir(runner_path):
        print(f"'{runner_name}' is already installed.")
    else:
        try:
            runner = import_runner(runner_name)
            runner().install(version=None, downloader=simple_downloader, callback=None)
            print(f"'{runner_name}' has been installed")
        except (InvalidRunner, RunnerInstallationError) as ex:
            print(ex.message)
install_runner(self, runner)
Source code in lutris/gui/application.py
def install_runner(self, runner):
    if runner.startswith("lutris"):
        self.install_wine_cli(runner)
    else:
        self.install_cli(runner)
install_wine_cli(self, version)

Downloads wine runner using lutris -r

Source code in lutris/gui/application.py
def install_wine_cli(self, version):
    """
    Downloads wine runner using lutris -r <runner>
    """

    WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
    runner_path = os.path.join(WINE_DIR, f"{version}{'' if '-x86_64' in version else '-x86_64'}")
    if os.path.isdir(runner_path):
        print(f"Wine version '{version}' is already installed.")
    else:

        try:
            runner = import_runner("wine")
            runner().install(downloader=simple_downloader, version=version)
            print(f"Wine version '{version}' has been installed.")
        except (InvalidRunner, RunnerInstallationError) as ex:
            print(ex.message)
on_app_window_destroyed(self, app_window, window_key)

Remove the reference to the window when it has been destroyed

Source code in lutris/gui/application.py
def on_app_window_destroyed(self, app_window, window_key):
    """Remove the reference to the window when it has been destroyed"""
    window_key = str(app_window.__class__.__name__) + window_key
    try:
        del self.app_windows[window_key]
        logger.debug("Removed window %s", window_key)
    except KeyError:
        logger.warning("Failed to remove window %s", window_key)
        logger.info("Available windows: %s", ", ".join(self.app_windows.keys()))
    return True
on_game_install(self, game)

Request installation of a game

Source code in lutris/gui/application.py
def on_game_install(self, game):
    """Request installation of a game"""
    if game.service and game.service != "lutris":
        service = get_enabled_services()[game.service]()
        db_game = ServiceGameCollection.get_game(service.id, game.appid)
        if not db_game:
            logger.error("Can't find %s for %s", game.name, service.name)
            return True
        try:
            game_id = service.install(db_game)
        except ValueError as e:
            logger.debug(e)
            game_id = None

        if game_id:
            game = Game(game_id)
            game.launch()
        return True
    if not game.slug:
        raise ValueError("Invalid game passed: %s" % game)
        # return True
    installers = get_installers(game_slug=game.slug)
    if installers:
        self.show_installer_window(installers)
    else:
        ErrorDialog(_("There is no installer available for %s.") % game.name, parent=self.window)
    return True
on_game_install_dlc(self, game)
Source code in lutris/gui/application.py
def on_game_install_dlc(self, game):
    service = get_enabled_services()[game.service]()
    db_game = games_db.get_game_by_field(game.id, "id")
    installers = service.get_dlc_installers(db_game)
    if installers:
        self.show_installer_window(installers, service, game.appid)
    else:
        ErrorDialog(_("No DLC found"))
    return True
on_game_install_update(self, game)
Source code in lutris/gui/application.py
def on_game_install_update(self, game):
    service = get_enabled_services()[game.service]()
    db_game = games_db.get_game_by_field(game.id, "id")
    installers = service.get_update_installers(db_game)
    if installers:
        self.show_installer_window(installers, service, game.appid, is_update=True)
    else:
        ErrorDialog(_("No updates found"))
    return True
on_game_launch(self, game)
Source code in lutris/gui/application.py
def on_game_launch(self, game):
    game.launch()
    return True  # Return True to continue handling the emission hook
on_game_start(self, game)
Source code in lutris/gui/application.py
def on_game_start(self, game):
    self.running_games.append(game)
    if settings.read_setting("hide_client_on_game_start") == "True":
        self.window.hide()  # Hide launcher window
    return True
on_game_stop(self, game)

Callback to remove the game from the running games

Source code in lutris/gui/application.py
def on_game_stop(self, game):
    """Callback to remove the game from the running games"""
    ids = self.get_running_game_ids()
    if str(game.id) in ids:
        try:
            self.running_games.remove(ids.index(str(game.id)))
        except ValueError:
            pass
    else:
        logger.warning("%s not in %s", game.id, ids)

    game.emit("game-stopped")
    if settings.read_setting("hide_client_on_game_start") == "True":
        self.window.show()  # Show launcher window
    elif not self.window.is_visible():
        if self.running_games.get_n_items() == 0:
            self.quit()
    return True
print_game_json(self, command_line, game_list)
Source code in lutris/gui/application.py
def print_game_json(self, command_line, game_list):
    games = [
        {
            "id": game["id"],
            "slug": game["slug"],
            "name": game["name"],
            "runner": game["runner"],
            "platform": game["platform"] or None,
            "year": game["year"] or None,
            "directory": game["directory"] or None,
            "hidden": bool(game["hidden"]),
            "playtime": (
                str(timedelta(hours=game["playtime"]))
                if game["playtime"] else None
            ),
            "lastplayed": (
                str(datetime.fromtimestamp(game["lastplayed"]))
                if game["lastplayed"] else None
            )
        } for game in game_list
    ]
    self._print(command_line, json.dumps(games, indent=2))
print_game_list(self, command_line, game_list)
Source code in lutris/gui/application.py
def print_game_list(self, command_line, game_list):
    for game in game_list:
        self._print(
            command_line,
            "{:4} | {:<40} | {:<40} | {:<15} | {:<64}".format(
                game["id"],
                game["name"][:40],
                game["slug"][:40],
                game["runner"] or "-",
                game["directory"] or "-",
            ),
        )
print_runners(self)
Source code in lutris/gui/application.py
def print_runners(self):
    runnersName = get_runner_names()
    sortednames = sorted(runnersName.keys(), key=lambda x: x.lower())
    for name in sortednames:
        print(name)
print_steam_folders(self, command_line)
Source code in lutris/gui/application.py
def print_steam_folders(self, command_line):
    steamapps_paths = get_steamapps_paths()
    for platform in ("linux", "windows"):
        for path in steamapps_paths[platform] if steamapps_paths else []:
            self._print(command_line, path)
print_steam_list(self, command_line)
Source code in lutris/gui/application.py
def print_steam_list(self, command_line):
    steamapps_paths = get_steamapps_paths()
    for path in steamapps_paths if steamapps_paths else []:
        appmanifest_files = get_appmanifests(path)
        for appmanifest_file in appmanifest_files:
            appmanifest = AppManifest(os.path.join(path, appmanifest_file))
            self._print(
                command_line,
                " {:8} | {:<60} | {}".format(
                    appmanifest.steamid,
                    appmanifest.name or "-",
                    ", ".join(appmanifest.states),
                ),
            )
print_wine_runners(self)
Source code in lutris/gui/application.py
def print_wine_runners(self):
    runnersName = get_runners("wine")
    for i in runnersName["versions"]:
        if i["version"]:
            print(i)
set_tray_icon(self)

Creates or destroys a tray icon for the application

Source code in lutris/gui/application.py
def set_tray_icon(self):
    """Creates or destroys a tray icon for the application"""
    active = settings.read_setting("show_tray_icon", default="false").lower() == "true"
    if active and not self.tray:
        self.tray = LutrisStatusIcon(application=self)
    if self.tray:
        self.tray.set_visible(active)
show_installer_window(self, installers, service=None, appid=None, is_update=False)
Source code in lutris/gui/application.py
def show_installer_window(self, installers, service=None, appid=None, is_update=False):
    self.show_window(
        InstallerWindow,
        installers=installers,
        service=service,
        appid=appid,
        is_update=is_update
    )
show_update_runtime_dialog() staticmethod
Source code in lutris/gui/application.py
@staticmethod
def show_update_runtime_dialog():
    if os.environ.get("LUTRIS_SKIP_INIT"):
        logger.debug("Skipping initialization")
    else:
        init_dialog = LutrisInitDialog(update_runtime)
        init_dialog.run()
show_window(self, window_class, **kwargs)

Instanciate a window keeping 1 instance max

Parameters:

Name Type Description Default
window_class Gtk.Window

class to create the instance from

required
kwargs dict

Additional arguments to pass to the instanciated window

{}

Returns:

Type Description
Gtk.Window

the existing window instance or a newly created one

Source code in lutris/gui/application.py
def show_window(self, window_class, **kwargs):
    """Instanciate a window keeping 1 instance max

    Params:
        window_class (Gtk.Window): class to create the instance from
        kwargs (dict): Additional arguments to pass to the instanciated window

    Returns:
        Gtk.Window: the existing window instance or a newly created one
    """
    window_key = str(window_class.__name__) + self.get_window_key(**kwargs)
    if self.app_windows.get(window_key):
        self.app_windows[window_key].present()
        return self.app_windows[window_key]
    if issubclass(window_class, Gtk.Dialog):
        if "parent" in kwargs:
            window_inst = window_class(**kwargs)
        else:
            window_inst = window_class(parent=self.window, **kwargs)
        window_inst.set_application(self)
    else:
        window_inst = window_class(application=self, **kwargs)
    window_inst.connect("destroy", self.on_app_window_destroyed, self.get_window_key(**kwargs))
    self.app_windows[window_key] = window_inst
    logger.debug("Showing window %s", window_key)
    window_inst.show()
    return window_inst
uninstall_runner(self, runner)
Source code in lutris/gui/application.py
def uninstall_runner(self, runner):
    if "wine" in runner:
        print("Are sure you want to delete Wine and all of the installed runners?[Y/N]")
        ans = input()
        if ans.lower() in ("y", "yes"):
            self.uninstall_runner_cli(runner)
        else:
            print("Not Removing Wine")
    elif runner.startswith("lutris"):
        self.wine_runner_uninstall(runner)
    else:
        self.uninstall_runner_cli(runner)
uninstall_runner_cli(self, runner_name)

uninstall the runner given in application file located in lutris/gui/application.py provided using lutris -u

Source code in lutris/gui/application.py
def uninstall_runner_cli(self, runner_name):
    """
    uninstall the runner given in application file located in lutris/gui/application.py
    provided using lutris -u <runner>
    """
    try:
        runner_class = import_runner(runner_name)
        runner = runner_class()
    except InvalidRunner:
        logger.error("Failed to import Runner: %s", runner_name)
        return
    if not runner.is_installed():
        print(f"Runner '{runner_name}' is not installed.")
        return
    if runner.can_uninstall():
        runner.uninstall()
        print(f"'{runner_name}' has been uninstalled.")
    else:
        print(f"Runner '{runner_name}' cannot be uninstalled.")
wine_runner_uninstall(self, version)
Source code in lutris/gui/application.py
    def wine_runner_uninstall(self, version):
        version = f"{version}{'' if '-x86_64' in version else '-x86_64'}"
        WINE_DIR = os.path.join(settings.RUNNER_DIR, "wine")
        runner_path = os.path.join(WINE_DIR, version)
        if os.path.isdir(runner_path):
            system.remove_folder(runner_path)
            print(f"Wine version '{version}' has been removed.")
        else:
            print(f"""
Specified version of Wine is not installed: {version}.
Please check if the Wine Runner and specified version are installed (for that use --list-wine-runners).
Also, check that the version specified is in the correct format.
                """)

config special

DIALOG_HEIGHT

DIALOG_WIDTH

add_game

AddGameDialog (GameDialogCommon)

Add game dialog class.

Source code in lutris/gui/config/add_game.py
class AddGameDialog(GameDialogCommon):
    """Add game dialog class."""

    def __init__(self, parent, game=None, runner=None):
        super().__init__(_("Add a new game"), parent=parent)
        self.game = game
        self.saved = False
        if game:
            self.runner_name = game.runner_name
            self.slug = game.slug
        else:
            self.runner_name = runner
            self.slug = None

        self.lutris_config = LutrisConfig(
            runner_slug=self.runner_name,
            level="game",
        )
        self.build_notebook()
        self.build_tabs("game")
        self.build_action_area(self.on_save)
        self.name_entry.grab_focus()
        self.connect("delete-event", self.on_cancel_clicked)
        self.show_all()
__init__(self, parent, game=None, runner=None) special
Source code in lutris/gui/config/add_game.py
def __init__(self, parent, game=None, runner=None):
    super().__init__(_("Add a new game"), parent=parent)
    self.game = game
    self.saved = False
    if game:
        self.runner_name = game.runner_name
        self.slug = game.slug
    else:
        self.runner_name = runner
        self.slug = None

    self.lutris_config = LutrisConfig(
        runner_slug=self.runner_name,
        level="game",
    )
    self.build_notebook()
    self.build_tabs("game")
    self.build_action_area(self.on_save)
    self.name_entry.grab_focus()
    self.connect("delete-event", self.on_cancel_clicked)
    self.show_all()

base_config_box

BaseConfigBox (VBox)
Source code in lutris/gui/config/base_config_box.py
class BaseConfigBox(VBox):

    def get_section_label(self, text):
        label = Gtk.Label(visible=True)
        label.set_markup("<b>%s</b>" % text)
        label.set_alignment(0, 0.5)
        label.set_margin_bottom(8)
        return label

    def get_description_label(self, text):
        label = Gtk.Label(visible=True)
        label.set_markup("%s" % text)
        label.set_line_wrap(True)
        label.set_alignment(0, 0.5)
        return label

    def __init__(self):
        super().__init__(visible=True)
        self.set_margin_top(50)
        self.set_margin_bottom(50)
        self.set_margin_right(80)
        self.set_margin_left(80)
__init__(self) special
Source code in lutris/gui/config/base_config_box.py
def __init__(self):
    super().__init__(visible=True)
    self.set_margin_top(50)
    self.set_margin_bottom(50)
    self.set_margin_right(80)
    self.set_margin_left(80)
get_description_label(self, text)
Source code in lutris/gui/config/base_config_box.py
def get_description_label(self, text):
    label = Gtk.Label(visible=True)
    label.set_markup("%s" % text)
    label.set_line_wrap(True)
    label.set_alignment(0, 0.5)
    return label
get_section_label(self, text)
Source code in lutris/gui/config/base_config_box.py
def get_section_label(self, text):
    label = Gtk.Label(visible=True)
    label.set_markup("<b>%s</b>" % text)
    label.set_alignment(0, 0.5)
    label.set_margin_bottom(8)
    return label

boxes

Widget generators and their signal handlers

ConfigBox (VBox)

Dynamically generate a vbox built upon on a python dict.

Source code in lutris/gui/config/boxes.py
class ConfigBox(VBox):

    """Dynamically generate a vbox built upon on a python dict."""

    def __init__(self, game=None):
        super().__init__()
        self.options = []
        self.game = game
        self.config = None
        self.raw_config = None
        self.option_widget = None
        self.wrapper = None
        self.tooltip_default = None
        self.files = []
        self.files_list_store = None

    def generate_top_info_box(self, text):
        """Add a top section with general help text for the current tab"""
        help_box = Gtk.Box()
        help_box.set_margin_left(15)
        help_box.set_margin_right(15)
        help_box.set_margin_bottom(5)

        icon = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.MENU)
        help_box.pack_start(icon, False, False, 5)

        title_label = Gtk.Label("<i>%s</i>" % text)
        title_label.set_line_wrap(True)
        title_label.set_alignment(0, 0.5)
        title_label.set_use_markup(True)
        help_box.pack_start(title_label, False, False, 5)

        self.pack_start(help_box, False, False, 0)
        self.pack_start(Gtk.HSeparator(), False, False, 12)

        help_box.show_all()

    def generate_widgets(self, config_section):  # noqa: C901 # pylint: disable=too-many-branches,too-many-statements
        """Parse the config dict and generates widget accordingly."""
        if not self.options:
            no_options_label = Label(_("No options available"))
            no_options_label.set_halign(Gtk.Align.CENTER)
            no_options_label.set_valign(Gtk.Align.CENTER)
            self.pack_start(no_options_label, True, True, 0)
            return

        # Select config section.
        if config_section == "game":
            self.config = self.lutris_config.game_config
            self.raw_config = self.lutris_config.raw_game_config
        elif config_section == "runner":
            self.config = self.lutris_config.runner_config
            self.raw_config = self.lutris_config.raw_runner_config
        elif config_section == "system":
            self.config = self.lutris_config.system_config
            self.raw_config = self.lutris_config.raw_system_config

        # Go thru all options.
        for option in self.options:
            if "scope" in option:
                if config_section not in option["scope"]:
                    continue
            option_key = option["option"]
            value = self.config.get(option_key)
            default = option.get("default")

            if callable(option.get("choices")) and option["type"] != "choice_with_search":
                option["choices"] = option["choices"]()
            if callable(option.get("condition")):
                option["condition"] = option["condition"]()

            self.wrapper = Gtk.Box()
            self.wrapper.set_spacing(12)
            self.wrapper.set_margin_bottom(6)

            # Set tooltip's "Default" part
            default = option.get("default")
            self.tooltip_default = default if isinstance(default, str) else None

            # Generate option widget
            self.option_widget = None
            self.call_widget_generator(option, option_key, value, default)

            # Reset button
            reset_btn = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
            reset_btn.set_relief(Gtk.ReliefStyle.NONE)
            reset_btn.set_tooltip_text(_("Reset option to global or default config"))
            reset_btn.connect(
                "clicked",
                self.on_reset_button_clicked,
                option,
                self.option_widget,
                self.wrapper,
            )

            placeholder = Gtk.Box()
            placeholder.set_size_request(32, 32)

            if option_key not in self.raw_config:
                reset_btn.set_visible(False)
                reset_btn.set_no_show_all(True)
            placeholder.pack_start(reset_btn, False, False, 0)

            # Tooltip
            helptext = option.get("help")
            if isinstance(self.tooltip_default, str):
                helptext = helptext + "\n\n" if helptext else ""
                helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
            if value != default and option_key not in self.raw_config:
                helptext = helptext + "\n\n" if helptext else ""
                helptext += _(
                    "<i>(Italic indicates that this option is "
                    "modified in a lower configuration level.)</i>"
                )
            if helptext:
                self.wrapper.props.has_tooltip = True
                self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)

            hbox = Gtk.Box()
            hbox.set_margin_left(18)
            hbox.pack_end(placeholder, False, False, 5)
            # Grey out option if condition unmet
            if "condition" in option and not option["condition"]:
                hbox.set_sensitive(False)

            # Hide if advanced
            if option.get("advanced"):
                hbox.get_style_context().add_class("advanced")
                show_advanced = settings.read_setting("show_advanced_options")
                if show_advanced != "True":
                    hbox.set_no_show_all(True)
            hbox.pack_start(self.wrapper, True, True, 0)
            self.pack_start(hbox, False, False, 0)

    def call_widget_generator(self, option, option_key, value, default):  # noqa: C901
        """Call the right generation method depending on option type."""
        # pylint: disable=too-many-branches
        option_type = option["type"]
        option_size = option.get("size", None)

        if option_key in self.raw_config:
            self.set_style_property("font-weight", "bold", self.wrapper)
        elif value != default:
            self.set_style_property("font-style", "italic", self.wrapper)

        if option_type == "choice":
            self.generate_combobox(option_key, option["choices"], option["label"], value, default)

        elif option_type == "choice_with_entry":
            self.generate_combobox(
                option_key,
                option["choices"],
                option["label"],
                value,
                default,
                has_entry=True,
            )
        elif option_type == "choice_with_search":
            self.generate_searchable_combobox(
                option_key,
                option["choices"],
                option["label"],
                value,
                default,
            )

        elif option_type == "bool":
            self.generate_checkbox(option, value)
            self.tooltip_default = "Enabled" if default else "Disabled"
        elif option_type == "extended_bool":
            self.generate_checkbox_with_callback(option, value)
            self.tooltip_default = "Enabled" if default else "Disabled"
        elif option_type == "range":
            self.generate_range(option_key, option["min"], option["max"], option["label"], value)
        elif option_type == "string":
            if "label" not in option:
                raise ValueError("Option %s has no label" % option)
            self.generate_entry(option_key, option["label"], value, option_size)
        elif option_type == "directory_chooser":
            self.generate_directory_chooser(option, value)
        elif option_type == "file":
            self.generate_file_chooser(option, value)
        elif option_type == "multiple":
            self.generate_multiple_file_chooser(option_key, option["label"], value)
        elif option_type == "label":
            self.generate_label(option["label"])
        elif option_type == "mapping":
            self.generate_editable_grid(option_key, label=option["label"], value=value)
        else:
            raise ValueError("Unknown widget type %s" % option_type)

    # Label
    def generate_label(self, text):
        """Generate a simple label."""
        label = Label(text)
        label.set_use_markup(True)
        label.set_halign(Gtk.Align.START)
        label.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, True, True, 0)

    # Checkbox
    def generate_checkbox(self, option, value=None):
        """Generate a checkbox."""

        label = Label(option["label"])
        self.wrapper.pack_start(label, False, False, 0)

        switch = Gtk.Switch()
        if value is True:
            switch.set_active(value)
        switch.connect("notify::active", self.checkbox_toggle, option["option"])
        switch.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(switch, False, False, 0)
        self.option_widget = switch

    # Checkbox with callback
    def generate_checkbox_with_callback(self, option, value=None):
        """Generate a checkbox. With callback"""

        label = Label(option["label"])
        self.wrapper.pack_start(label, False, False, 0)

        checkbox = Gtk.Switch()
        checkbox.set_sensitive(option["active"] is True)
        if value is True:
            checkbox.set_active(value)

        checkbox.connect("notify::active", self._on_toggle_with_callback, option)
        checkbox.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(checkbox, False, False, 0)
        self.option_widget = checkbox

    def checkbox_toggle(self, widget, _gparam, option_name):
        """Action for the checkbox's toggled signal."""
        self.option_changed(widget, option_name, widget.get_active())

    def _on_toggle_with_callback(self, widget, _gparam, option):
        """Action for the checkbox's toggled signal. With callback method"""

        option_name = option["option"]
        callback = option["callback"]
        callback_on = option.get("callback_on")
        if widget.get_active() == callback_on or callback_on is None:
            AsyncCall(callback, self._on_callback_finished, widget, option, self.config)
        else:
            self.option_changed(widget, option_name, widget.get_active())

    def _on_callback_finished(self, result, _error):
        widget, option, response = result
        if response:
            self.option_changed(widget, option["option"], widget.get_active())
        else:
            widget.set_active(False)

    # Entry
    def generate_entry(self, option_name, label, value=None, option_size=None):
        """Generate an entry box."""
        label = Label(label)
        self.wrapper.pack_start(label, False, False, 0)

        entry = Gtk.Entry()
        if value:
            entry.set_text(value)
        entry.connect("changed", self.entry_changed, option_name)
        expand = option_size != "small"
        self.wrapper.pack_start(entry, expand, expand, 0)
        self.option_widget = entry

    def entry_changed(self, entry, option_name):
        """Action triggered for entry 'changed' signal."""
        self.option_changed(entry, option_name, entry.get_text())

    def generate_searchable_combobox(self, option_name, choice_func, label, value, default):
        """Generate a searchable combo box"""
        combobox = SearchableCombobox(choice_func, value or default)
        combobox.connect("changed", self.on_searchable_entry_changed, option_name)
        self.wrapper.pack_start(Label(label), False, False, 0)
        self.wrapper.pack_start(combobox, True, True, 0)
        self.option_widget = combobox

    def on_searchable_entry_changed(self, combobox, value, key):
        self.option_changed(combobox, key, value)

    def _populate_combobox_choices(self, liststore, choices, default):
        for choice in choices:
            if isinstance(choice, str):
                choice = (choice, choice)
            if choice[1] == default:
                liststore.append((_("%s (default)") % choice[0], choice[1]))
                self.tooltip_default = choice[0]
            else:
                liststore.append(choice)

    # ComboBox
    def generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False):
        """Generate a combobox (drop-down menu)."""
        liststore = Gtk.ListStore(str, str)
        self._populate_combobox_choices(liststore, choices, default)
        # With entry ("choice_with_entry" type)
        if has_entry:
            combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
            combobox.set_entry_text_column(0)
            if value:
                combobox.get_child().set_text(value)
        # No entry ("choice" type)
        else:
            combobox = Gtk.ComboBox.new_with_model(liststore)
            cell = Gtk.CellRendererText()
            combobox.pack_start(cell, True)
            combobox.add_attribute(cell, "text", 0)
            combobox.set_id_column(1)

            choices = list(v for k, v in choices)
            if value in choices:
                combobox.set_active_id(value)
            else:
                combobox.set_active_id(default)

        combobox.connect("changed", self.on_combobox_change, option_name)
        combobox.connect("scroll-event", self._on_combobox_scroll)
        label = Label(label)
        combobox.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(combobox, True, True, 0)
        self.option_widget = combobox

    @staticmethod
    def _on_combobox_scroll(combobox, _event):
        """Prevents users from accidentally changing configuration values
        while scrolling down dialogs.
        """
        combobox.stop_emission_by_name("scroll-event")
        return False

    def on_combobox_change(self, combobox, option):
        """Action triggered on combobox 'changed' signal."""
        list_store = combobox.get_model()
        active = combobox.get_active()
        option_value = None
        if active < 0:
            if combobox.get_has_entry():
                option_value = combobox.get_child().get_text()
        else:
            option_value = list_store[active][1]
        self.option_changed(combobox, option, option_value)

    # Range
    def generate_range(self, option_name, min_val, max_val, label, value=None):
        """Generate a ranged spin button."""
        adjustment = Gtk.Adjustment(float(min_val), float(min_val), float(max_val), 1, 0, 0)
        spin_button = Gtk.SpinButton()
        spin_button.set_adjustment(adjustment)
        if value:
            spin_button.set_value(value)
        spin_button.connect("changed", self.on_spin_button_changed, option_name)
        label = Label(label)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(spin_button, True, True, 0)
        self.option_widget = spin_button

    def on_spin_button_changed(self, spin_button, option):
        """Action triggered on spin button 'changed' signal."""
        value = spin_button.get_value_as_int()
        self.option_changed(spin_button, option, value)

    # File chooser
    def generate_file_chooser(self, option, path=None):
        """Generate a file chooser button to select a file."""
        option_name = option["option"]
        label = Label(option["label"])
        default_path = option.get("default_path") or (self.runner.default_path if self.runner else "")
        file_chooser = FileChooserEntry(
            title=_("Select file"),
            action=Gtk.FileChooserAction.OPEN,
            path=path,
            default_path=default_path
        )
        # file_chooser.set_size_request(200, 30)

        if "default_path" in option:
            default_path = self.lutris_config.system_config.get(option["default_path"])
            if default_path and os.path.exists(default_path):
                file_chooser.entry.set_text(default_path)

        if path:
            # If path is relative, complete with game dir
            if not os.path.isabs(path):
                path = os.path.expanduser(path)
                if not os.path.isabs(path):
                    if self.game and self.game.directory:
                        path = os.path.join(self.game.directory, path)
            file_chooser.entry.set_text(path)

        file_chooser.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(file_chooser, True, True, 0)
        self.option_widget = file_chooser
        file_chooser.entry.connect("changed", self._on_chooser_file_set, option_name)

    def _on_chooser_file_set(self, entry, option):
        """Action triggered on file select dialog 'file-set' signal."""
        if not os.path.isabs(entry.get_text()):
            entry.set_text(os.path.expanduser(entry.get_text()))
        self.option_changed(entry.get_parent(), option, entry.get_text())

    # Directory chooser
    def generate_directory_chooser(self, option, path=None):
        """Generate a file chooser button to select a directory."""
        label = Label(option["label"])
        option_name = option["option"]
        default_path = None
        if not path and self.game and self.game.runner:
            default_path = self.game.runner.working_dir
        directory_chooser = FileChooserEntry(
            title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=path, default_path=default_path
        )
        directory_chooser.entry.connect("changed", self._on_chooser_dir_set, option_name)
        directory_chooser.set_valign(Gtk.Align.CENTER)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(directory_chooser, True, True, 0)
        self.option_widget = directory_chooser

    def _on_chooser_dir_set(self, entry, option):
        """Action triggered on file select dialog 'file-set' signal."""
        self.option_changed(entry.get_parent(), option, entry.get_text())

    # Editable grid
    def generate_editable_grid(self, option_name, label, value=None):
        """Adds an editable grid widget"""
        value = value or {}
        try:
            value = list(value.items())
        except AttributeError:
            logger.error("Invalid value of type %s passed to grid widget: %s", type(value), value)
            value = {}
        label = Label(label)

        grid = EditableGrid(value, columns=["Key", "Value"])
        grid.connect("changed", self._on_grid_changed, option_name)
        self.wrapper.pack_start(label, False, False, 0)
        self.wrapper.pack_start(grid, True, True, 0)
        self.option_widget = grid
        return grid

    def _on_grid_changed(self, grid, option):
        values = dict(grid.get_data())
        self.option_changed(grid, option, values)

    # Multiple file selector
    def generate_multiple_file_chooser(self, option_name, label, value=None):
        """Generate a multiple file selector."""
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        label = Label(label + ":")
        label.set_halign(Gtk.Align.START)
        button = Gtk.Button(_("Add files"))
        button.connect("clicked", self.on_add_files_clicked, option_name, value)
        button.set_margin_left(10)
        vbox.pack_start(label, False, False, 5)
        vbox.pack_end(button, False, False, 0)

        if value:
            if isinstance(value, str):
                self.files = [value]
            else:
                self.files = value
        else:
            self.files = []
        self.files_list_store = Gtk.ListStore(str)
        for filename in self.files:
            self.files_list_store.append([filename])
        cell_renderer = Gtk.CellRendererText()
        files_treeview = Gtk.TreeView(self.files_list_store)
        files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0)
        files_treeview.append_column(files_column)
        files_treeview.connect("key-press-event", self.on_files_treeview_keypress, option_name)
        treeview_scroll = Gtk.ScrolledWindow()
        treeview_scroll.set_min_content_height(130)
        treeview_scroll.set_margin_left(10)
        treeview_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        treeview_scroll.add(files_treeview)

        vbox.pack_start(treeview_scroll, True, True, 0)
        self.wrapper.pack_start(vbox, True, True, 0)
        self.option_widget = self.files_list_store

    def on_add_files_clicked(self, _widget, option_name, value):
        """Create and run multi-file chooser dialog."""
        dialog = Gtk.FileChooserNative.new(
            _("Select files"),
            None,
            Gtk.FileChooserAction.OPEN,
            _("_Add"),
            _("_Cancel"),
        )
        dialog.set_select_multiple(True)

        first_file_dir = os.path.dirname(value[0]) if value else None
        dialog.set_current_folder(
            first_file_dir or self.game.directory or self.config.get("game_path") or os.path.expanduser("~")
        )
        response = dialog.run()
        if response == Gtk.ResponseType.ACCEPT:
            self.add_files_to_treeview(dialog, option_name, self.wrapper)
        dialog.destroy()

    def add_files_to_treeview(self, dialog, option, wrapper):
        """Add several files to the configuration"""
        filenames = dialog.get_filenames()
        files = self.config.get(option, [])
        for filename in filenames:
            self.files_list_store.append([filename])
            if filename not in files:
                files.append(filename)
        self.option_changed(wrapper, option, files)

    def on_files_treeview_keypress(self, treeview, event, option):
        """Action triggered when a row is deleted from the filechooser."""
        key = event.keyval
        if key == Gdk.KEY_Delete:
            selection = treeview.get_selection()
            (model, treepaths) = selection.get_selected_rows()
            for treepath in treepaths:
                row_index = int(str(treepath))
                treeiter = model.get_iter(treepath)
                model.remove(treeiter)
                self.raw_config[option].pop(row_index)

    @staticmethod
    def on_query_tooltip(_widget, x, y, keybmode, tooltip, text):  # pylint: disable=unused-argument
        """Prepare a custom tooltip with a fixed width"""
        label = Label(text)
        label.set_use_markup(True)
        label.set_max_width_chars(60)
        hbox = Gtk.Box()
        hbox.pack_start(label, False, False, 0)
        hbox.show_all()
        tooltip.set_custom(hbox)
        return True

    def option_changed(self, widget, option_name, value):
        """Common actions when value changed on a widget"""
        self.raw_config[option_name] = value
        self.config[option_name] = value

        wrapper = widget.get_parent()
        hbox = wrapper.get_parent()

        # Dirty way to get the reset btn. I tried passing it through the
        # methods but got some strange unreliable behavior.
        reset_btn = hbox.get_children()[1].get_children()[0]
        reset_btn.set_visible(True)
        self.set_style_property("font-weight", "bold", wrapper)

    def on_reset_button_clicked(self, btn, option, _widget, wrapper):
        """Clear option (remove from config, reset option widget)."""
        option_key = option["option"]
        current_value = self.config[option_key]

        btn.set_visible(False)
        self.set_style_property("font-weight", "normal", wrapper)
        self.raw_config.pop(option_key)
        self.lutris_config.update_cascaded_config()

        reset_value = self.config.get(option_key)
        if current_value == reset_value:
            return

        # Destroy and recreate option widget
        self.wrapper = wrapper
        children = wrapper.get_children()
        for child in children:
            child.destroy()
        self.call_widget_generator(option, option_key, reset_value, option.get("default"))
        self.wrapper.show_all()

    @staticmethod
    def set_style_property(property_, value, wrapper):
        """Add custom style."""
        style_provider = Gtk.CssProvider()
        style_provider.load_from_data("GtkHBox {{{}: {};}}".format(property_, value).encode())
        style_context = wrapper.get_style_context()
        style_context.add_provider(style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
__init__(self, game=None) special
Source code in lutris/gui/config/boxes.py
def __init__(self, game=None):
    super().__init__()
    self.options = []
    self.game = game
    self.config = None
    self.raw_config = None
    self.option_widget = None
    self.wrapper = None
    self.tooltip_default = None
    self.files = []
    self.files_list_store = None
add_files_to_treeview(self, dialog, option, wrapper)

Add several files to the configuration

Source code in lutris/gui/config/boxes.py
def add_files_to_treeview(self, dialog, option, wrapper):
    """Add several files to the configuration"""
    filenames = dialog.get_filenames()
    files = self.config.get(option, [])
    for filename in filenames:
        self.files_list_store.append([filename])
        if filename not in files:
            files.append(filename)
    self.option_changed(wrapper, option, files)
call_widget_generator(self, option, option_key, value, default)

Call the right generation method depending on option type.

Source code in lutris/gui/config/boxes.py
def call_widget_generator(self, option, option_key, value, default):  # noqa: C901
    """Call the right generation method depending on option type."""
    # pylint: disable=too-many-branches
    option_type = option["type"]
    option_size = option.get("size", None)

    if option_key in self.raw_config:
        self.set_style_property("font-weight", "bold", self.wrapper)
    elif value != default:
        self.set_style_property("font-style", "italic", self.wrapper)

    if option_type == "choice":
        self.generate_combobox(option_key, option["choices"], option["label"], value, default)

    elif option_type == "choice_with_entry":
        self.generate_combobox(
            option_key,
            option["choices"],
            option["label"],
            value,
            default,
            has_entry=True,
        )
    elif option_type == "choice_with_search":
        self.generate_searchable_combobox(
            option_key,
            option["choices"],
            option["label"],
            value,
            default,
        )

    elif option_type == "bool":
        self.generate_checkbox(option, value)
        self.tooltip_default = "Enabled" if default else "Disabled"
    elif option_type == "extended_bool":
        self.generate_checkbox_with_callback(option, value)
        self.tooltip_default = "Enabled" if default else "Disabled"
    elif option_type == "range":
        self.generate_range(option_key, option["min"], option["max"], option["label"], value)
    elif option_type == "string":
        if "label" not in option:
            raise ValueError("Option %s has no label" % option)
        self.generate_entry(option_key, option["label"], value, option_size)
    elif option_type == "directory_chooser":
        self.generate_directory_chooser(option, value)
    elif option_type == "file":
        self.generate_file_chooser(option, value)
    elif option_type == "multiple":
        self.generate_multiple_file_chooser(option_key, option["label"], value)
    elif option_type == "label":
        self.generate_label(option["label"])
    elif option_type == "mapping":
        self.generate_editable_grid(option_key, label=option["label"], value=value)
    else:
        raise ValueError("Unknown widget type %s" % option_type)
checkbox_toggle(self, widget, _gparam, option_name)

Action for the checkbox's toggled signal.

Source code in lutris/gui/config/boxes.py
def checkbox_toggle(self, widget, _gparam, option_name):
    """Action for the checkbox's toggled signal."""
    self.option_changed(widget, option_name, widget.get_active())
entry_changed(self, entry, option_name)

Action triggered for entry 'changed' signal.

Source code in lutris/gui/config/boxes.py
def entry_changed(self, entry, option_name):
    """Action triggered for entry 'changed' signal."""
    self.option_changed(entry, option_name, entry.get_text())
generate_checkbox(self, option, value=None)

Generate a checkbox.

Source code in lutris/gui/config/boxes.py
def generate_checkbox(self, option, value=None):
    """Generate a checkbox."""

    label = Label(option["label"])
    self.wrapper.pack_start(label, False, False, 0)

    switch = Gtk.Switch()
    if value is True:
        switch.set_active(value)
    switch.connect("notify::active", self.checkbox_toggle, option["option"])
    switch.set_valign(Gtk.Align.CENTER)
    self.wrapper.pack_start(switch, False, False, 0)
    self.option_widget = switch
generate_checkbox_with_callback(self, option, value=None)

Generate a checkbox. With callback

Source code in lutris/gui/config/boxes.py
def generate_checkbox_with_callback(self, option, value=None):
    """Generate a checkbox. With callback"""

    label = Label(option["label"])
    self.wrapper.pack_start(label, False, False, 0)

    checkbox = Gtk.Switch()
    checkbox.set_sensitive(option["active"] is True)
    if value is True:
        checkbox.set_active(value)

    checkbox.connect("notify::active", self._on_toggle_with_callback, option)
    checkbox.set_valign(Gtk.Align.CENTER)
    self.wrapper.pack_start(checkbox, False, False, 0)
    self.option_widget = checkbox
generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False)

Generate a combobox (drop-down menu).

Source code in lutris/gui/config/boxes.py
def generate_combobox(self, option_name, choices, label, value=None, default=None, has_entry=False):
    """Generate a combobox (drop-down menu)."""
    liststore = Gtk.ListStore(str, str)
    self._populate_combobox_choices(liststore, choices, default)
    # With entry ("choice_with_entry" type)
    if has_entry:
        combobox = Gtk.ComboBox.new_with_model_and_entry(liststore)
        combobox.set_entry_text_column(0)
        if value:
            combobox.get_child().set_text(value)
    # No entry ("choice" type)
    else:
        combobox = Gtk.ComboBox.new_with_model(liststore)
        cell = Gtk.CellRendererText()
        combobox.pack_start(cell, True)
        combobox.add_attribute(cell, "text", 0)
        combobox.set_id_column(1)

        choices = list(v for k, v in choices)
        if value in choices:
            combobox.set_active_id(value)
        else:
            combobox.set_active_id(default)

    combobox.connect("changed", self.on_combobox_change, option_name)
    combobox.connect("scroll-event", self._on_combobox_scroll)
    label = Label(label)
    combobox.set_valign(Gtk.Align.CENTER)
    self.wrapper.pack_start(label, False, False, 0)
    self.wrapper.pack_start(combobox, True, True, 0)
    self.option_widget = combobox
generate_directory_chooser(self, option, path=None)

Generate a file chooser button to select a directory.

Source code in lutris/gui/config/boxes.py
def generate_directory_chooser(self, option, path=None):
    """Generate a file chooser button to select a directory."""
    label = Label(option["label"])
    option_name = option["option"]
    default_path = None
    if not path and self.game and self.game.runner:
        default_path = self.game.runner.working_dir
    directory_chooser = FileChooserEntry(
        title=_("Select folder"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=path, default_path=default_path
    )
    directory_chooser.entry.connect("changed", self._on_chooser_dir_set, option_name)
    directory_chooser.set_valign(Gtk.Align.CENTER)
    self.wrapper.pack_start(label, False, False, 0)
    self.wrapper.pack_start(directory_chooser, True, True, 0)
    self.option_widget = directory_chooser
generate_editable_grid(self, option_name, label, value=None)

Adds an editable grid widget

Source code in lutris/gui/config/boxes.py
def generate_editable_grid(self, option_name, label, value=None):
    """Adds an editable grid widget"""
    value = value or {}
    try:
        value = list(value.items())
    except AttributeError:
        logger.error("Invalid value of type %s passed to grid widget: %s", type(value), value)
        value = {}
    label = Label(label)

    grid = EditableGrid(value, columns=["Key", "Value"])
    grid.connect("changed", self._on_grid_changed, option_name)
    self.wrapper.pack_start(label, False, False, 0)
    self.wrapper.pack_start(grid, True, True, 0)
    self.option_widget = grid
    return grid
generate_entry(self, option_name, label, value=None, option_size=None)

Generate an entry box.

Source code in lutris/gui/config/boxes.py
def generate_entry(self, option_name, label, value=None, option_size=None):
    """Generate an entry box."""
    label = Label(label)
    self.wrapper.pack_start(label, False, False, 0)

    entry = Gtk.Entry()
    if value:
        entry.set_text(value)
    entry.connect("changed", self.entry_changed, option_name)
    expand = option_size != "small"
    self.wrapper.pack_start(entry, expand, expand, 0)
    self.option_widget = entry
generate_file_chooser(self, option, path=None)

Generate a file chooser button to select a file.

Source code in lutris/gui/config/boxes.py
def generate_file_chooser(self, option, path=None):
    """Generate a file chooser button to select a file."""
    option_name = option["option"]
    label = Label(option["label"])
    default_path = option.get("default_path") or (self.runner.default_path if self.runner else "")
    file_chooser = FileChooserEntry(
        title=_("Select file"),
        action=Gtk.FileChooserAction.OPEN,
        path=path,
        default_path=default_path
    )
    # file_chooser.set_size_request(200, 30)

    if "default_path" in option:
        default_path = self.lutris_config.system_config.get(option["default_path"])
        if default_path and os.path.exists(default_path):
            file_chooser.entry.set_text(default_path)

    if path:
        # If path is relative, complete with game dir
        if not os.path.isabs(path):
            path = os.path.expanduser(path)
            if not os.path.isabs(path):
                if self.game and self.game.directory:
                    path = os.path.join(self.game.directory, path)
        file_chooser.entry.set_text(path)

    file_chooser.set_valign(Gtk.Align.CENTER)
    self.wrapper.pack_start(label, False, False, 0)
    self.wrapper.pack_start(file_chooser, True, True, 0)
    self.option_widget = file_chooser
    file_chooser.entry.connect("changed", self._on_chooser_file_set, option_name)
generate_label(self, text)

Generate a simple label.

Source code in lutris/gui/config/boxes.py
def generate_label(self, text):
    """Generate a simple label."""
    label = Label(text)
    label.set_use_markup(True)
    label.set_halign(Gtk.Align.START)
    label.set_valign(Gtk.Align.CENTER)
    self.wrapper.pack_start(label, True, True, 0)
generate_multiple_file_chooser(self, option_name, label, value=None)

Generate a multiple file selector.

Source code in lutris/gui/config/boxes.py
def generate_multiple_file_chooser(self, option_name, label, value=None):
    """Generate a multiple file selector."""
    vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    label = Label(label + ":")
    label.set_halign(Gtk.Align.START)
    button = Gtk.Button(_("Add files"))
    button.connect("clicked", self.on_add_files_clicked, option_name, value)
    button.set_margin_left(10)
    vbox.pack_start(label, False, False, 5)
    vbox.pack_end(button, False, False, 0)

    if value:
        if isinstance(value, str):
            self.files = [value]
        else:
            self.files = value
    else:
        self.files = []
    self.files_list_store = Gtk.ListStore(str)
    for filename in self.files:
        self.files_list_store.append([filename])
    cell_renderer = Gtk.CellRendererText()
    files_treeview = Gtk.TreeView(self.files_list_store)
    files_column = Gtk.TreeViewColumn(_("Files"), cell_renderer, text=0)
    files_treeview.append_column(files_column)
    files_treeview.connect("key-press-event", self.on_files_treeview_keypress, option_name)
    treeview_scroll = Gtk.ScrolledWindow()
    treeview_scroll.set_min_content_height(130)
    treeview_scroll.set_margin_left(10)
    treeview_scroll.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
    treeview_scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    treeview_scroll.add(files_treeview)

    vbox.pack_start(treeview_scroll, True, True, 0)
    self.wrapper.pack_start(vbox, True, True, 0)
    self.option_widget = self.files_list_store
generate_range(self, option_name, min_val, max_val, label, value=None)

Generate a ranged spin button.

Source code in lutris/gui/config/boxes.py
def generate_range(self, option_name, min_val, max_val, label, value=None):
    """Generate a ranged spin button."""
    adjustment = Gtk.Adjustment(float(min_val), float(min_val), float(max_val), 1, 0, 0)
    spin_button = Gtk.SpinButton()
    spin_button.set_adjustment(adjustment)
    if value:
        spin_button.set_value(value)
    spin_button.connect("changed", self.on_spin_button_changed, option_name)
    label = Label(label)
    self.wrapper.pack_start(label, False, False, 0)
    self.wrapper.pack_start(spin_button, True, True, 0)
    self.option_widget = spin_button
generate_searchable_combobox(self, option_name, choice_func, label, value, default)

Generate a searchable combo box

Source code in lutris/gui/config/boxes.py
def generate_searchable_combobox(self, option_name, choice_func, label, value, default):
    """Generate a searchable combo box"""
    combobox = SearchableCombobox(choice_func, value or default)
    combobox.connect("changed", self.on_searchable_entry_changed, option_name)
    self.wrapper.pack_start(Label(label), False, False, 0)
    self.wrapper.pack_start(combobox, True, True, 0)
    self.option_widget = combobox
generate_top_info_box(self, text)

Add a top section with general help text for the current tab

Source code in lutris/gui/config/boxes.py
def generate_top_info_box(self, text):
    """Add a top section with general help text for the current tab"""
    help_box = Gtk.Box()
    help_box.set_margin_left(15)
    help_box.set_margin_right(15)
    help_box.set_margin_bottom(5)

    icon = Gtk.Image.new_from_icon_name("dialog-information", Gtk.IconSize.MENU)
    help_box.pack_start(icon, False, False, 5)

    title_label = Gtk.Label("<i>%s</i>" % text)
    title_label.set_line_wrap(True)
    title_label.set_alignment(0, 0.5)
    title_label.set_use_markup(True)
    help_box.pack_start(title_label, False, False, 5)

    self.pack_start(help_box, False, False, 0)
    self.pack_start(Gtk.HSeparator(), False, False, 12)

    help_box.show_all()
generate_widgets(self, config_section)

Parse the config dict and generates widget accordingly.

Source code in lutris/gui/config/boxes.py
def generate_widgets(self, config_section):  # noqa: C901 # pylint: disable=too-many-branches,too-many-statements
    """Parse the config dict and generates widget accordingly."""
    if not self.options:
        no_options_label = Label(_("No options available"))
        no_options_label.set_halign(Gtk.Align.CENTER)
        no_options_label.set_valign(Gtk.Align.CENTER)
        self.pack_start(no_options_label, True, True, 0)
        return

    # Select config section.
    if config_section == "game":
        self.config = self.lutris_config.game_config
        self.raw_config = self.lutris_config.raw_game_config
    elif config_section == "runner":
        self.config = self.lutris_config.runner_config
        self.raw_config = self.lutris_config.raw_runner_config
    elif config_section == "system":
        self.config = self.lutris_config.system_config
        self.raw_config = self.lutris_config.raw_system_config

    # Go thru all options.
    for option in self.options:
        if "scope" in option:
            if config_section not in option["scope"]:
                continue
        option_key = option["option"]
        value = self.config.get(option_key)
        default = option.get("default")

        if callable(option.get("choices")) and option["type"] != "choice_with_search":
            option["choices"] = option["choices"]()
        if callable(option.get("condition")):
            option["condition"] = option["condition"]()

        self.wrapper = Gtk.Box()
        self.wrapper.set_spacing(12)
        self.wrapper.set_margin_bottom(6)

        # Set tooltip's "Default" part
        default = option.get("default")
        self.tooltip_default = default if isinstance(default, str) else None

        # Generate option widget
        self.option_widget = None
        self.call_widget_generator(option, option_key, value, default)

        # Reset button
        reset_btn = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
        reset_btn.set_relief(Gtk.ReliefStyle.NONE)
        reset_btn.set_tooltip_text(_("Reset option to global or default config"))
        reset_btn.connect(
            "clicked",
            self.on_reset_button_clicked,
            option,
            self.option_widget,
            self.wrapper,
        )

        placeholder = Gtk.Box()
        placeholder.set_size_request(32, 32)

        if option_key not in self.raw_config:
            reset_btn.set_visible(False)
            reset_btn.set_no_show_all(True)
        placeholder.pack_start(reset_btn, False, False, 0)

        # Tooltip
        helptext = option.get("help")
        if isinstance(self.tooltip_default, str):
            helptext = helptext + "\n\n" if helptext else ""
            helptext += _("<b>Default</b>: ") + _(self.tooltip_default)
        if value != default and option_key not in self.raw_config:
            helptext = helptext + "\n\n" if helptext else ""
            helptext += _(
                "<i>(Italic indicates that this option is "
                "modified in a lower configuration level.)</i>"
            )
        if helptext:
            self.wrapper.props.has_tooltip = True
            self.wrapper.connect("query-tooltip", self.on_query_tooltip, helptext)

        hbox = Gtk.Box()
        hbox.set_margin_left(18)
        hbox.pack_end(placeholder, False, False, 5)
        # Grey out option if condition unmet
        if "condition" in option and not option["condition"]:
            hbox.set_sensitive(False)

        # Hide if advanced
        if option.get("advanced"):
            hbox.get_style_context().add_class("advanced")
            show_advanced = settings.read_setting("show_advanced_options")
            if show_advanced != "True":
                hbox.set_no_show_all(True)
        hbox.pack_start(self.wrapper, True, True, 0)
        self.pack_start(hbox, False, False, 0)
on_add_files_clicked(self, _widget, option_name, value)

Create and run multi-file chooser dialog.

Source code in lutris/gui/config/boxes.py
def on_add_files_clicked(self, _widget, option_name, value):
    """Create and run multi-file chooser dialog."""
    dialog = Gtk.FileChooserNative.new(
        _("Select files"),
        None,
        Gtk.FileChooserAction.OPEN,
        _("_Add"),
        _("_Cancel"),
    )
    dialog.set_select_multiple(True)

    first_file_dir = os.path.dirname(value[0]) if value else None
    dialog.set_current_folder(
        first_file_dir or self.game.directory or self.config.get("game_path") or os.path.expanduser("~")
    )
    response = dialog.run()
    if response == Gtk.ResponseType.ACCEPT:
        self.add_files_to_treeview(dialog, option_name, self.wrapper)
    dialog.destroy()
on_combobox_change(self, combobox, option)

Action triggered on combobox 'changed' signal.

Source code in lutris/gui/config/boxes.py
def on_combobox_change(self, combobox, option):
    """Action triggered on combobox 'changed' signal."""
    list_store = combobox.get_model()
    active = combobox.get_active()
    option_value = None
    if active < 0:
        if combobox.get_has_entry():
            option_value = combobox.get_child().get_text()
    else:
        option_value = list_store[active][1]
    self.option_changed(combobox, option, option_value)
on_files_treeview_keypress(self, treeview, event, option)

Action triggered when a row is deleted from the filechooser.

Source code in lutris/gui/config/boxes.py
def on_files_treeview_keypress(self, treeview, event, option):
    """Action triggered when a row is deleted from the filechooser."""
    key = event.keyval
    if key == Gdk.KEY_Delete:
        selection = treeview.get_selection()
        (model, treepaths) = selection.get_selected_rows()
        for treepath in treepaths:
            row_index = int(str(treepath))
            treeiter = model.get_iter(treepath)
            model.remove(treeiter)
            self.raw_config[option].pop(row_index)
on_query_tooltip(_widget, x, y, keybmode, tooltip, text) staticmethod

Prepare a custom tooltip with a fixed width

Source code in lutris/gui/config/boxes.py
@staticmethod
def on_query_tooltip(_widget, x, y, keybmode, tooltip, text):  # pylint: disable=unused-argument
    """Prepare a custom tooltip with a fixed width"""
    label = Label(text)
    label.set_use_markup(True)
    label.set_max_width_chars(60)
    hbox = Gtk.Box()
    hbox.pack_start(label, False, False, 0)
    hbox.show_all()
    tooltip.set_custom(hbox)
    return True
on_reset_button_clicked(self, btn, option, _widget, wrapper)

Clear option (remove from config, reset option widget).

Source code in lutris/gui/config/boxes.py
def on_reset_button_clicked(self, btn, option, _widget, wrapper):
    """Clear option (remove from config, reset option widget)."""
    option_key = option["option"]
    current_value = self.config[option_key]

    btn.set_visible(False)
    self.set_style_property("font-weight", "normal", wrapper)
    self.raw_config.pop(option_key)
    self.lutris_config.update_cascaded_config()

    reset_value = self.config.get(option_key)
    if current_value == reset_value:
        return

    # Destroy and recreate option widget
    self.wrapper = wrapper
    children = wrapper.get_children()
    for child in children:
        child.destroy()
    self.call_widget_generator(option, option_key, reset_value, option.get("default"))
    self.wrapper.show_all()
on_searchable_entry_changed(self, combobox, value, key)
Source code in lutris/gui/config/boxes.py
def on_searchable_entry_changed(self, combobox, value, key):
    self.option_changed(combobox, key, value)
on_spin_button_changed(self, spin_button, option)

Action triggered on spin button 'changed' signal.

Source code in lutris/gui/config/boxes.py
def on_spin_button_changed(self, spin_button, option):
    """Action triggered on spin button 'changed' signal."""
    value = spin_button.get_value_as_int()
    self.option_changed(spin_button, option, value)
option_changed(self, widget, option_name, value)

Common actions when value changed on a widget

Source code in lutris/gui/config/boxes.py
def option_changed(self, widget, option_name, value):
    """Common actions when value changed on a widget"""
    self.raw_config[option_name] = value
    self.config[option_name] = value

    wrapper = widget.get_parent()
    hbox = wrapper.get_parent()

    # Dirty way to get the reset btn. I tried passing it through the
    # methods but got some strange unreliable behavior.
    reset_btn = hbox.get_children()[1].get_children()[0]
    reset_btn.set_visible(True)
    self.set_style_property("font-weight", "bold", wrapper)
set_style_property(property_, value, wrapper) staticmethod

Add custom style.

Source code in lutris/gui/config/boxes.py
@staticmethod
def set_style_property(property_, value, wrapper):
    """Add custom style."""
    style_provider = Gtk.CssProvider()
    style_provider.load_from_data("GtkHBox {{{}: {};}}".format(property_, value).encode())
    style_context = wrapper.get_style_context()
    style_context.add_provider(style_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
GameBox (ConfigBox)
Source code in lutris/gui/config/boxes.py
class GameBox(ConfigBox):

    def __init__(self, lutris_config, game):
        ConfigBox.__init__(self, game)
        self.lutris_config = lutris_config
        if game.runner_name:
            if not game.runner:
                try:
                    self.runner = import_runner(game.runner_name)()
                except InvalidRunner:
                    self.runner = None
            else:
                self.runner = game.runner
            if self.runner:
                self.options = self.runner.game_options
        else:
            logger.warning("No runner in game supplied to GameBox")
        self.generate_widgets("game")
__init__(self, lutris_config, game) special
Source code in lutris/gui/config/boxes.py
def __init__(self, lutris_config, game):
    ConfigBox.__init__(self, game)
    self.lutris_config = lutris_config
    if game.runner_name:
        if not game.runner:
            try:
                self.runner = import_runner(game.runner_name)()
            except InvalidRunner:
                self.runner = None
        else:
            self.runner = game.runner
        if self.runner:
            self.options = self.runner.game_options
    else:
        logger.warning("No runner in game supplied to GameBox")
    self.generate_widgets("game")
RunnerBox (ConfigBox)

Configuration box for runner specific options

Source code in lutris/gui/config/boxes.py
class RunnerBox(ConfigBox):

    """Configuration box for runner specific options"""

    def __init__(self, lutris_config, game=None):
        ConfigBox.__init__(self, game)
        self.lutris_config = lutris_config
        try:
            self.runner = import_runner(self.lutris_config.runner_slug)()
        except InvalidRunner:
            self.runner = None
        if self.runner:
            self.options = self.runner.get_runner_options()

        if lutris_config.level == "game":
            self.generate_top_info_box(_(
                "If modified, these options supersede the same options from "
                "the base runner configuration."
            ))
        self.generate_widgets("runner")
__init__(self, lutris_config, game=None) special
Source code in lutris/gui/config/boxes.py
def __init__(self, lutris_config, game=None):
    ConfigBox.__init__(self, game)
    self.lutris_config = lutris_config
    try:
        self.runner = import_runner(self.lutris_config.runner_slug)()
    except InvalidRunner:
        self.runner = None
    if self.runner:
        self.options = self.runner.get_runner_options()

    if lutris_config.level == "game":
        self.generate_top_info_box(_(
            "If modified, these options supersede the same options from "
            "the base runner configuration."
        ))
    self.generate_widgets("runner")
SystemBox (ConfigBox)
Source code in lutris/gui/config/boxes.py
class SystemBox(ConfigBox):

    def __init__(self, lutris_config):
        ConfigBox.__init__(self)
        self.lutris_config = lutris_config
        self.runner = None
        runner_slug = self.lutris_config.runner_slug

        if runner_slug:
            self.options = sysoptions.with_runner_overrides(runner_slug)
        else:
            self.options = sysoptions.system_options

        if lutris_config.game_config_id and runner_slug:
            self.generate_top_info_box(_(
                "If modified, these options supersede the same options from "
                "the base runner configuration, which themselves supersede "
                "the global preferences."
            ))
        elif runner_slug:
            self.generate_top_info_box(_(
                "If modified, these options supersede the same options from "
                "the global preferences."
            ))

        self.generate_widgets("system")
__init__(self, lutris_config) special
Source code in lutris/gui/config/boxes.py
def __init__(self, lutris_config):
    ConfigBox.__init__(self)
    self.lutris_config = lutris_config
    self.runner = None
    runner_slug = self.lutris_config.runner_slug

    if runner_slug:
        self.options = sysoptions.with_runner_overrides(runner_slug)
    else:
        self.options = sysoptions.system_options

    if lutris_config.game_config_id and runner_slug:
        self.generate_top_info_box(_(
            "If modified, these options supersede the same options from "
            "the base runner configuration, which themselves supersede "
            "the global preferences."
        ))
    elif runner_slug:
        self.generate_top_info_box(_(
            "If modified, these options supersede the same options from "
            "the global preferences."
        ))

    self.generate_widgets("system")

common

Shared config dialog stuff

GameDialogCommon (Dialog)

Base class for config dialogs

Source code in lutris/gui/config/common.py
class GameDialogCommon(Dialog):
    """Base class for config dialogs"""
    no_runner_label = _("Select a runner in the Game Info tab")

    def __init__(self, title, parent=None):
        super().__init__(title, parent=parent)
        self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
        self.set_default_size(DIALOG_WIDTH, DIALOG_HEIGHT)
        self.notebook = None
        self.name_entry = None
        self.runner_box = None

        self.timer_id = None
        self.game = None
        self.saved = None
        self.slug = None
        self.slug_entry = None
        self.directory_entry = None
        self.year_entry = None
        self.slug_change_button = None
        self.runner_dropdown = None
        self.banner_button = None
        self.icon_button = None
        self.game_box = None
        self.system_box = None
        self.runner_name = None
        self.runner_index = None
        self.lutris_config = None

        # These are independent windows, but start centered over
        # a parent like a dialog. Not modal, not really transient,
        # and does not share modality with other windows - so it
        # needs its own window group.
        Gtk.WindowGroup().add_window(self)
        GLib.idle_add(self.clear_transient_for)

    def clear_transient_for(self):
        # we need the parent set to be centered over the parent, but
        # we don't want to be transient really- we want other windows
        # able to come to the front.
        self.set_transient_for(None)
        return False

    @staticmethod
    def build_scrolled_window(widget):
        """Return a scrolled window containing config widgets"""
        scrolled_window = Gtk.ScrolledWindow(visible=True)
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_window.add(widget)
        return scrolled_window

    def build_notebook(self):
        self.notebook = Gtk.Notebook(visible=True)
        self.notebook.set_show_border(False)
        self.vbox.pack_start(self.notebook, True, True, 10)

    def build_tabs(self, config_level):
        """Build tabs (for game and runner levels)"""
        self.timer_id = None
        if config_level == "game":
            self._build_info_tab()
            self._build_game_tab()
        self._build_runner_tab(config_level)
        self._build_system_tab(config_level)

    def _build_info_tab(self):
        info_box = VBox()

        if self.game:
            info_box.pack_start(self._get_banner_box(), False, False, 6)  # Banner

        info_box.pack_start(self._get_name_box(), False, False, 6)  # Game name

        self.runner_box = self._get_runner_box()
        info_box.pack_start(self.runner_box, False, False, 6)  # Runner

        info_box.pack_start(self._get_year_box(), False, False, 6)  # Year

        if self.game:
            info_box.pack_start(self._get_slug_box(), False, False, 6)
            info_box.pack_start(self._get_directory_box(), False, False, 6)

        info_sw = self.build_scrolled_window(info_box)
        self._add_notebook_tab(info_sw, _("Game info"))

    def _get_name_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        label = Label(_("Name"))
        box.pack_start(label, False, False, 0)
        self.name_entry = Gtk.Entry()
        if self.game:
            self.name_entry.set_text(self.game.name)
        box.pack_start(self.name_entry, True, True, 0)
        return box

    def _get_slug_box(self):
        slug_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label(_("Identifier"))
        slug_box.pack_start(label, False, False, 0)

        self.slug_entry = SlugEntry()
        self.slug_entry.set_text(self.game.slug)
        self.slug_entry.set_sensitive(False)
        self.slug_entry.connect("activate", self.on_slug_entry_activate)
        slug_box.pack_start(self.slug_entry, True, True, 0)

        self.slug_change_button = Gtk.Button(_("Change"))
        self.slug_change_button.connect("clicked", self.on_slug_change_clicked)
        slug_box.pack_start(self.slug_change_button, False, False, 0)

        return slug_box

    def _get_directory_box(self):
        """Return widget displaying the location of the game and allowing to move it"""
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12, visible=True)
        label = Label(_("Directory"))
        box.pack_start(label, False, False, 0)
        self.directory_entry = Gtk.Entry(visible=True)
        self.directory_entry.set_text(self.game.directory)
        self.directory_entry.set_sensitive(False)
        box.pack_start(self.directory_entry, True, True, 0)
        move_button = Gtk.Button(_("Move"), visible=True)
        move_button.connect("clicked", self.on_move_clicked)
        box.pack_start(move_button, False, False, 0)
        return box

    def _get_runner_box(self):
        runner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        runner_label = Label(_("Runner"))
        runner_box.pack_start(runner_label, False, False, 0)

        self.runner_dropdown = self._get_runner_dropdown()
        runner_box.pack_start(self.runner_dropdown, True, True, 0)

        return runner_box

    def _get_banner_box(self):
        banner_box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label("")
        banner_box.pack_start(label, False, False, 0)

        self.banner_button = Gtk.Button()
        self._set_image("banner")
        self.banner_button.connect("clicked", self.on_custom_image_select, "banner")
        banner_box.pack_start(self.banner_button, False, False, 0)

        reset_banner_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
        reset_banner_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_banner_button.set_tooltip_text(_("Remove custom banner"))
        reset_banner_button.connect("clicked", self.on_custom_image_reset_clicked, "banner")
        banner_box.pack_start(reset_banner_button, False, False, 0)

        self.icon_button = Gtk.Button()
        self._set_image("icon")
        self.icon_button.connect("clicked", self.on_custom_image_select, "icon")
        banner_box.pack_start(self.icon_button, False, False, 0)

        reset_icon_button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU)
        reset_icon_button.set_relief(Gtk.ReliefStyle.NONE)
        reset_icon_button.set_tooltip_text(_("Remove custom icon"))
        reset_icon_button.connect("clicked", self.on_custom_image_reset_clicked, "icon")
        banner_box.pack_start(reset_icon_button, False, False, 0)

        return banner_box

    def _get_year_box(self):
        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)

        label = Label(_("Release year"))
        box.pack_start(label, False, False, 0)

        self.year_entry = NumberEntry()
        if self.game:
            self.year_entry.set_text(str(self.game.year or ""))
        box.pack_start(self.year_entry, True, True, 0)

        return box

    def _set_image(self, image_format):
        image = Gtk.Image()
        service_media = LutrisBanner() if image_format == "banner" else LutrisIcon()
        game_slug = self.game.slug if self.game else ""
        image.set_from_pixbuf(service_media.get_pixbuf_for_game(game_slug))
        if image_format == "banner":
            self.banner_button.set_image(image)
        else:
            self.icon_button.set_image(image)

    def _get_runner_dropdown(self):
        runner_liststore = self._get_runner_liststore()
        runner_dropdown = Gtk.ComboBox.new_with_model(runner_liststore)
        runner_dropdown.set_id_column(1)
        runner_index = 0
        if self.runner_name:
            for runner in runner_liststore:
                if self.runner_name == str(runner[1]):
                    break
                runner_index += 1
        self.runner_index = runner_index
        runner_dropdown.set_active(self.runner_index)
        runner_dropdown.connect("changed", self.on_runner_changed)
        cell = Gtk.CellRendererText()
        cell.props.ellipsize = Pango.EllipsizeMode.END
        runner_dropdown.pack_start(cell, True)
        runner_dropdown.add_attribute(cell, "text", 0)
        return runner_dropdown

    @staticmethod
    def _get_runner_liststore():
        """Build a ListStore with available runners."""
        runner_liststore = Gtk.ListStore(str, str)
        runner_liststore.append((_("Select a runner from the list"), ""))
        for runner in runners.get_installed():
            description = runner.description
            runner_liststore.append(("%s (%s)" % (runner.human_name, description), runner.name))
        return runner_liststore

    def on_slug_change_clicked(self, widget):
        if self.slug_entry.get_sensitive() is False:
            widget.set_label(_("Apply"))
            self.slug_entry.set_sensitive(True)
        else:
            self.change_game_slug()

    def on_slug_entry_activate(self, _widget):
        self.change_game_slug()

    def change_game_slug(self):
        self.slug = self.slug_entry.get_text()
        self.slug_entry.set_sensitive(False)
        self.slug_change_button.set_label(_("Change"))

    def on_move_clicked(self, _button):
        new_location = DirectoryDialog("Select new location for the game",
                                       default_path=self.game.directory, parent=self)
        if not new_location.folder or new_location.folder == self.game.directory:
            return
        move_dialog = dialogs.MoveDialog(self.game, new_location.folder)
        move_dialog.connect("game-moved", self.on_game_moved)
        move_dialog.move()

    def on_game_moved(self, dialog):
        """Show a notification when the game is moved"""
        new_directory = dialog.new_directory
        if new_directory:
            self.directory_entry.set_text(new_directory)
            send_notification("Finished moving game", "%s moved to %s" % (dialog.game, new_directory))
        else:
            send_notification("Failed to move game", "Lutris could not move %s" % dialog.game)

    def _build_game_tab(self):
        if self.game and self.runner_name:
            self.game.runner_name = self.runner_name
            if not self.game.runner or self.game.runner.name != self.runner_name:
                try:
                    self.game.runner = runners.import_runner(self.runner_name)()
                except runners.InvalidRunner:
                    pass
            self.game_box = GameBox(self.lutris_config, self.game)
            game_sw = self.build_scrolled_window(self.game_box)
        elif self.runner_name:
            game = Game(None)
            game.runner_name = self.runner_name
            self.game_box = GameBox(self.lutris_config, game)
            game_sw = self.build_scrolled_window(self.game_box)
        else:
            game_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(game_sw, _("Game options"))

    def _build_runner_tab(self, _config_level):
        if self.runner_name:
            self.runner_box = RunnerBox(self.lutris_config, self.game)
            runner_sw = self.build_scrolled_window(self.runner_box)
        else:
            runner_sw = Gtk.Label(label=self.no_runner_label)
        self._add_notebook_tab(runner_sw, _("Runner options"))

    def _build_system_tab(self, _config_level):
        if not self.lutris_config:
            raise RuntimeError("Lutris config not loaded yet")
        self.system_box = SystemBox(self.lutris_config)
        self._add_notebook_tab(
            self.build_scrolled_window(self.system_box),
            _("System options")
        )

    def _add_notebook_tab(self, widget, label):
        self.notebook.append_page(widget, Gtk.Label(label=label))

    def build_action_area(self, button_callback):
        self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

        # Advanced settings checkbox
        checkbox = Gtk.CheckButton(label=_("Show advanced options"))
        if settings.read_setting("show_advanced_options") == "True":
            checkbox.set_active(True)
        checkbox.connect("toggled", self.on_show_advanced_options_toggled)
        self.action_area.pack_start(checkbox, False, False, 5)

        # Buttons
        hbox = Gtk.Box()
        cancel_button = Gtk.Button(label=_("Cancel"))
        cancel_button.connect("clicked", self.on_cancel_clicked)
        hbox.pack_start(cancel_button, True, True, 10)

        save_button = Gtk.Button(label=_("Save"))
        save_button.connect("clicked", button_callback)
        hbox.pack_start(save_button, True, True, 0)
        self.action_area.pack_start(hbox, True, True, 0)

    def on_show_advanced_options_toggled(self, checkbox):
        value = bool(checkbox.get_active())
        settings.write_setting("show_advanced_options", value)

        self._set_advanced_options_visible(value)

    def _set_advanced_options_visible(self, value):
        """Change visibility of advanced options across all config tabs."""
        widgets = self.system_box.get_children()
        if self.runner_name:
            widgets += self.runner_box.get_children()
        if self.game:
            widgets += self.game_box.get_children()

        for widget in widgets:
            if widget.get_style_context().has_class("advanced"):
                widget.set_visible(value)
                if value:
                    widget.set_no_show_all(not value)
                    widget.show_all()

    def on_runner_changed(self, widget):
        """Action called when runner drop down is changed."""
        new_runner_index = widget.get_active()
        if self.runner_index and new_runner_index != self.runner_index:
            dlg = QuestionDialog(
                {
                    "parent": self,
                    "question":
                    _("Are you sure you want to change the runner for this game ? "
                      "This will reset the full configuration for this game and "
                      "is not reversible."),
                    "title":
                    _("Confirm runner change"),
                }
            )

            if dlg.result == Gtk.ResponseType.YES:
                self.runner_index = new_runner_index
                self._switch_runner(widget)
            else:
                # Revert the dropdown menu to the previously selected runner
                widget.set_active(self.runner_index)
        else:
            self.runner_index = new_runner_index
            self._switch_runner(widget)

    def _switch_runner(self, widget):
        """Rebuilds the UI on runner change"""
        current_page = self.notebook.get_current_page()
        if self.runner_index == 0:
            logger.info("No runner selected, resetting configuration")
            self.runner_name = None
            self.lutris_config = None
        else:
            runner_name = widget.get_model()[self.runner_index][1]
            if runner_name == self.runner_name:
                logger.debug("Runner unchanged, not creating a new config")
                return
            logger.info("Creating new configuration with runner %s", runner_name)
            self.runner_name = runner_name
            self.lutris_config = LutrisConfig(runner_slug=self.runner_name, level="game")
        self._rebuild_tabs()
        self.notebook.set_current_page(current_page)

    def _rebuild_tabs(self):
        for i in range(self.notebook.get_n_pages(), 1, -1):
            self.notebook.remove_page(i - 1)
        self._build_game_tab()
        self._build_runner_tab("game")
        self._build_system_tab("game")
        self.show_all()

    def on_cancel_clicked(self, _widget=None, _event=None):
        """Dialog destroy callback."""
        if self.game:
            self.game.load_config()
        self.destroy()

    def is_valid(self):
        if not self.runner_name:
            ErrorDialog(_("Runner not provided"), parent=self)
            return False
        if not self.name_entry.get_text():
            ErrorDialog(_("Please fill in the name"), parent=self)
            return False
        if self.runner_name == "steam" and not self.lutris_config.game_config.get("appid"):
            ErrorDialog(_("Steam AppID not provided"), parent=self)
            return False
        invalid_fields = []
        runner_class = import_runner(self.runner_name)
        runner_instance = runner_class()
        for config in ["game", "runner"]:
            for k, v in getattr(self.lutris_config, config + "_config").items():
                option = runner_instance.find_option(config + "_options", k)
                if option is None:
                    continue
                validator = option.get("validator")
                if validator is not None:
                    try:
                        res = validator(v)
                        logger.debug("%s validated successfully: %s", k, res)
                    except Exception:
                        invalid_fields.append(option.get("label"))
        if invalid_fields:
            ErrorDialog(_("The following fields have invalid values: ") + ", ".join(invalid_fields), parent=self)
            return False
        return True

    def on_save(self, _button):
        """Save game info and destroy widget. Return True if success."""
        if not self.is_valid():
            logger.warning(_("Current configuration is not valid, ignoring save request"))
            return False
        name = self.name_entry.get_text()

        if not self.slug:
            self.slug = slugify(name)

        if not self.game:
            self.game = Game()

        year = None
        if self.year_entry.get_text():
            year = int(self.year_entry.get_text())

        if not self.lutris_config.game_config_id:
            self.lutris_config.game_config_id = make_game_config_id(self.slug)

        runner_class = runners.import_runner(self.runner_name)
        runner = runner_class(self.lutris_config)

        self.game.name = name
        self.game.slug = self.slug
        self.game.year = year
        self.game.game_config_id = self.lutris_config.game_config_id
        self.game.runner = runner
        self.game.runner_name = self.runner_name
        self.game.is_installed = True
        self.game.config = self.lutris_config
        self.game.save(save_config=True)
        self.destroy()
        self.saved = True
        return True

    def on_custom_image_select(self, _widget, image_type):
        dialog = Gtk.FileChooserNative.new(
            _("Please choose a custom image"),
            self,
            Gtk.FileChooserAction.OPEN,
            None,
            None,
        )

        image_filter = Gtk.FileFilter()
        image_filter.set_name(_("Images"))
        image_filter.add_pixbuf_formats()
        dialog.add_filter(image_filter)

        response = dialog.run()
        if response == Gtk.ResponseType.ACCEPT:
            image_path = dialog.get_filename()
            if image_type == "banner":
                self.game.has_custom_banner = True
                dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
                size = BANNER_SIZE
                file_format = "jpeg"
            else:
                self.game.has_custom_icon = True
                dest_path = resources.get_icon_path(self.game.slug)
                size = ICON_SIZE
                file_format = "png"
            pixbuf = get_pixbuf(image_path, size)
            pixbuf.savev(dest_path, file_format, [], [])
            self._set_image(image_type)

            if image_type == "icon":
                system.update_desktop_icons()

        dialog.destroy()

    def on_custom_image_reset_clicked(self, _widget, image_type):
        if image_type == "banner":
            self.game.has_custom_banner = False
            dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
        elif image_type == "icon":
            self.game.has_custom_icon = False
            dest_path = resources.get_icon_path(self.game.slug)
        else:
            raise ValueError("Unsupported image type %s" % image_type)
        if os.path.isfile(dest_path):
            os.remove(dest_path)
        self._set_image(image_type)
no_runner_label
__init__(self, title, parent=None) special
Source code in lutris/gui/config/common.py
def __init__(self, title, parent=None):
    super().__init__(title, parent=parent)
    self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
    self.set_default_size(DIALOG_WIDTH, DIALOG_HEIGHT)
    self.notebook = None
    self.name_entry = None
    self.runner_box = None

    self.timer_id = None
    self.game = None
    self.saved = None
    self.slug = None
    self.slug_entry = None
    self.directory_entry = None
    self.year_entry = None
    self.slug_change_button = None
    self.runner_dropdown = None
    self.banner_button = None
    self.icon_button = None
    self.game_box = None
    self.system_box = None
    self.runner_name = None
    self.runner_index = None
    self.lutris_config = None

    # These are independent windows, but start centered over
    # a parent like a dialog. Not modal, not really transient,
    # and does not share modality with other windows - so it
    # needs its own window group.
    Gtk.WindowGroup().add_window(self)
    GLib.idle_add(self.clear_transient_for)
build_action_area(self, button_callback)
Source code in lutris/gui/config/common.py
def build_action_area(self, button_callback):
    self.action_area.set_layout(Gtk.ButtonBoxStyle.EDGE)

    # Advanced settings checkbox
    checkbox = Gtk.CheckButton(label=_("Show advanced options"))
    if settings.read_setting("show_advanced_options") == "True":
        checkbox.set_active(True)
    checkbox.connect("toggled", self.on_show_advanced_options_toggled)
    self.action_area.pack_start(checkbox, False, False, 5)

    # Buttons
    hbox = Gtk.Box()
    cancel_button = Gtk.Button(label=_("Cancel"))
    cancel_button.connect("clicked", self.on_cancel_clicked)
    hbox.pack_start(cancel_button, True, True, 10)

    save_button = Gtk.Button(label=_("Save"))
    save_button.connect("clicked", button_callback)
    hbox.pack_start(save_button, True, True, 0)
    self.action_area.pack_start(hbox, True, True, 0)
build_notebook(self)
Source code in lutris/gui/config/common.py
def build_notebook(self):
    self.notebook = Gtk.Notebook(visible=True)
    self.notebook.set_show_border(False)
    self.vbox.pack_start(self.notebook, True, True, 10)
build_scrolled_window(widget) staticmethod

Return a scrolled window containing config widgets

Source code in lutris/gui/config/common.py
@staticmethod
def build_scrolled_window(widget):
    """Return a scrolled window containing config widgets"""
    scrolled_window = Gtk.ScrolledWindow(visible=True)
    scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    scrolled_window.add(widget)
    return scrolled_window
build_tabs(self, config_level)

Build tabs (for game and runner levels)

Source code in lutris/gui/config/common.py
def build_tabs(self, config_level):
    """Build tabs (for game and runner levels)"""
    self.timer_id = None
    if config_level == "game":
        self._build_info_tab()
        self._build_game_tab()
    self._build_runner_tab(config_level)
    self._build_system_tab(config_level)
change_game_slug(self)
Source code in lutris/gui/config/common.py
def change_game_slug(self):
    self.slug = self.slug_entry.get_text()
    self.slug_entry.set_sensitive(False)
    self.slug_change_button.set_label(_("Change"))
clear_transient_for(self)
Source code in lutris/gui/config/common.py
def clear_transient_for(self):
    # we need the parent set to be centered over the parent, but
    # we don't want to be transient really- we want other windows
    # able to come to the front.
    self.set_transient_for(None)
    return False
is_valid(self)
Source code in lutris/gui/config/common.py
def is_valid(self):
    if not self.runner_name:
        ErrorDialog(_("Runner not provided"), parent=self)
        return False
    if not self.name_entry.get_text():
        ErrorDialog(_("Please fill in the name"), parent=self)
        return False
    if self.runner_name == "steam" and not self.lutris_config.game_config.get("appid"):
        ErrorDialog(_("Steam AppID not provided"), parent=self)
        return False
    invalid_fields = []
    runner_class = import_runner(self.runner_name)
    runner_instance = runner_class()
    for config in ["game", "runner"]:
        for k, v in getattr(self.lutris_config, config + "_config").items():
            option = runner_instance.find_option(config + "_options", k)
            if option is None:
                continue
            validator = option.get("validator")
            if validator is not None:
                try:
                    res = validator(v)
                    logger.debug("%s validated successfully: %s", k, res)
                except Exception:
                    invalid_fields.append(option.get("label"))
    if invalid_fields:
        ErrorDialog(_("The following fields have invalid values: ") + ", ".join(invalid_fields), parent=self)
        return False
    return True
on_cancel_clicked(self, _widget=None, _event=None)

Dialog destroy callback.

Source code in lutris/gui/config/common.py
def on_cancel_clicked(self, _widget=None, _event=None):
    """Dialog destroy callback."""
    if self.game:
        self.game.load_config()
    self.destroy()
on_custom_image_reset_clicked(self, _widget, image_type)
Source code in lutris/gui/config/common.py
def on_custom_image_reset_clicked(self, _widget, image_type):
    if image_type == "banner":
        self.game.has_custom_banner = False
        dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
    elif image_type == "icon":
        self.game.has_custom_icon = False
        dest_path = resources.get_icon_path(self.game.slug)
    else:
        raise ValueError("Unsupported image type %s" % image_type)
    if os.path.isfile(dest_path):
        os.remove(dest_path)
    self._set_image(image_type)
on_custom_image_select(self, _widget, image_type)
Source code in lutris/gui/config/common.py
def on_custom_image_select(self, _widget, image_type):
    dialog = Gtk.FileChooserNative.new(
        _("Please choose a custom image"),
        self,
        Gtk.FileChooserAction.OPEN,
        None,
        None,
    )

    image_filter = Gtk.FileFilter()
    image_filter.set_name(_("Images"))
    image_filter.add_pixbuf_formats()
    dialog.add_filter(image_filter)

    response = dialog.run()
    if response == Gtk.ResponseType.ACCEPT:
        image_path = dialog.get_filename()
        if image_type == "banner":
            self.game.has_custom_banner = True
            dest_path = os.path.join(settings.BANNER_PATH, "%s.jpg" % self.game.slug)
            size = BANNER_SIZE
            file_format = "jpeg"
        else:
            self.game.has_custom_icon = True
            dest_path = resources.get_icon_path(self.game.slug)
            size = ICON_SIZE
            file_format = "png"
        pixbuf = get_pixbuf(image_path, size)
        pixbuf.savev(dest_path, file_format, [], [])
        self._set_image(image_type)

        if image_type == "icon":
            system.update_desktop_icons()

    dialog.destroy()
on_game_moved(self, dialog)

Show a notification when the game is moved

Source code in lutris/gui/config/common.py
def on_game_moved(self, dialog):
    """Show a notification when the game is moved"""
    new_directory = dialog.new_directory
    if new_directory:
        self.directory_entry.set_text(new_directory)
        send_notification("Finished moving game", "%s moved to %s" % (dialog.game, new_directory))
    else:
        send_notification("Failed to move game", "Lutris could not move %s" % dialog.game)
on_move_clicked(self, _button)
Source code in lutris/gui/config/common.py
def on_move_clicked(self, _button):
    new_location = DirectoryDialog("Select new location for the game",
                                   default_path=self.game.directory, parent=self)
    if not new_location.folder or new_location.folder == self.game.directory:
        return
    move_dialog = dialogs.MoveDialog(self.game, new_location.folder)
    move_dialog.connect("game-moved", self.on_game_moved)
    move_dialog.move()
on_runner_changed(self, widget)

Action called when runner drop down is changed.

Source code in lutris/gui/config/common.py
def on_runner_changed(self, widget):
    """Action called when runner drop down is changed."""
    new_runner_index = widget.get_active()
    if self.runner_index and new_runner_index != self.runner_index:
        dlg = QuestionDialog(
            {
                "parent": self,
                "question":
                _("Are you sure you want to change the runner for this game ? "
                  "This will reset the full configuration for this game and "
                  "is not reversible."),
                "title":
                _("Confirm runner change"),
            }
        )

        if dlg.result == Gtk.ResponseType.YES:
            self.runner_index = new_runner_index
            self._switch_runner(widget)
        else:
            # Revert the dropdown menu to the previously selected runner
            widget.set_active(self.runner_index)
    else:
        self.runner_index = new_runner_index
        self._switch_runner(widget)
on_save(self, _button)

Save game info and destroy widget. Return True if success.

Source code in lutris/gui/config/common.py
def on_save(self, _button):
    """Save game info and destroy widget. Return True if success."""
    if not self.is_valid():
        logger.warning(_("Current configuration is not valid, ignoring save request"))
        return False
    name = self.name_entry.get_text()

    if not self.slug:
        self.slug = slugify(name)

    if not self.game:
        self.game = Game()

    year = None
    if self.year_entry.get_text():
        year = int(self.year_entry.get_text())

    if not self.lutris_config.game_config_id:
        self.lutris_config.game_config_id = make_game_config_id(self.slug)

    runner_class = runners.import_runner(self.runner_name)
    runner = runner_class(self.lutris_config)

    self.game.name = name
    self.game.slug = self.slug
    self.game.year = year
    self.game.game_config_id = self.lutris_config.game_config_id
    self.game.runner = runner
    self.game.runner_name = self.runner_name
    self.game.is_installed = True
    self.game.config = self.lutris_config
    self.game.save(save_config=True)
    self.destroy()
    self.saved = True
    return True
on_show_advanced_options_toggled(self, checkbox)
Source code in lutris/gui/config/common.py
def on_show_advanced_options_toggled(self, checkbox):
    value = bool(checkbox.get_active())
    settings.write_setting("show_advanced_options", value)

    self._set_advanced_options_visible(value)
on_slug_change_clicked(self, widget)
Source code in lutris/gui/config/common.py
def on_slug_change_clicked(self, widget):
    if self.slug_entry.get_sensitive() is False:
        widget.set_label(_("Apply"))
        self.slug_entry.set_sensitive(True)
    else:
        self.change_game_slug()
on_slug_entry_activate(self, _widget)
Source code in lutris/gui/config/common.py
def on_slug_entry_activate(self, _widget):
    self.change_game_slug()

edit_game

EditGameConfigDialog (GameDialogCommon)

Game config edit dialog.

Source code in lutris/gui/config/edit_game.py
class EditGameConfigDialog(GameDialogCommon):
    """Game config edit dialog."""

    def __init__(self, parent, game):
        super().__init__(_("Configure %s") % game.name, parent=parent)
        self.game = game
        self.lutris_config = game.config
        self.slug = game.slug
        self.runner_name = game.runner_name
        self.build_notebook()
        self.build_tabs("game")
        self.build_action_area(self.on_save)
        self.connect("delete-event", self.on_cancel_clicked)
        self.show_all()
__init__(self, parent, game) special
Source code in lutris/gui/config/edit_game.py
def __init__(self, parent, game):
    super().__init__(_("Configure %s") % game.name, parent=parent)
    self.game = game
    self.lutris_config = game.config
    self.slug = game.slug
    self.runner_name = game.runner_name
    self.build_notebook()
    self.build_tabs("game")
    self.build_action_area(self.on_save)
    self.connect("delete-event", self.on_cancel_clicked)
    self.show_all()

preferences_box

PreferencesBox (VBox)
Source code in lutris/gui/config/preferences_box.py
class PreferencesBox(VBox):
    settings_options = {
        "hide_client_on_game_start": _("Minimize client when a game is launched"),
        "hide_text_under_icons": _("Hide text under icons (requires restart)"),
        "show_tray_icon": _("Show Tray Icon (requires restart)"),
        "dark_theme": _("Use dark theme (requires dark theme variant for Gtk)")
    }

    def _get_section_label(self, text):
        label = Gtk.Label(visible=True)
        label.set_markup("<b>%s</b>" % text)
        label.set_alignment(0, 0.5)
        return label

    def __init__(self):
        super().__init__(visible=True)
        self.set_margin_top(50)
        self.set_margin_bottom(50)
        self.set_margin_right(80)
        self.set_margin_left(80)
        self.add(self._get_section_label(_("Interface options")))
        listbox = Gtk.ListBox(visible=True)
        self.pack_start(listbox, False, False, 12)
        for setting_key, label in self.settings_options.items():
            list_box_row = Gtk.ListBoxRow(visible=True)
            list_box_row.set_selectable(False)
            list_box_row.set_activatable(False)
            list_box_row.add(self._get_setting_box(setting_key, label))
            listbox.add(list_box_row)

    def _get_setting_box(self, setting_key, label):
        box = Gtk.Box(
            spacing=12,
            margin_top=12,
            margin_bottom=12,
            visible=True
        )
        label = Gtk.Label(label, visible=True)
        label.set_alignment(0, 0.5)
        box.pack_start(label, True, True, 12)
        checkbox = Gtk.Switch(visible=True)
        if settings.read_setting(setting_key).lower() == "true":
            checkbox.set_active(True)
        checkbox.connect("state-set", self._on_setting_change, setting_key)
        box.pack_start(checkbox, False, False, 12)
        return box

    def _on_setting_change(self, widget, state, setting_key):
        """Save a setting when an option is toggled"""
        settings.write_setting(setting_key, state)

        if setting_key == "dark_theme":
            application = Gio.Application.get_default()
            application.style_manager.is_config_dark = state
settings_options
__init__(self) special
Source code in lutris/gui/config/preferences_box.py
def __init__(self):
    super().__init__(visible=True)
    self.set_margin_top(50)
    self.set_margin_bottom(50)
    self.set_margin_right(80)
    self.set_margin_left(80)
    self.add(self._get_section_label(_("Interface options")))
    listbox = Gtk.ListBox(visible=True)
    self.pack_start(listbox, False, False, 12)
    for setting_key, label in self.settings_options.items():
        list_box_row = Gtk.ListBoxRow(visible=True)
        list_box_row.set_selectable(False)
        list_box_row.set_activatable(False)
        list_box_row.add(self._get_setting_box(setting_key, label))
        listbox.add(list_box_row)

preferences_dialog

Configuration dialog for client and system options

PreferencesDialog (GameDialogCommon)
Source code in lutris/gui/config/preferences_dialog.py
class PreferencesDialog(GameDialogCommon):
    def __init__(self, parent=None):
        super().__init__(_("Lutris settings"), parent=parent)
        self.set_border_width(0)
        self.set_default_size(1010, 600)
        self.lutris_config = LutrisConfig()

        hbox = Gtk.HBox(visible=True)
        sidebar = Gtk.ListBox(visible=True)
        sidebar.connect("row-selected", self.on_sidebar_activated)
        sidebar.add(self.get_sidebar_button("prefs-stack", _("Interface"), "view-grid-symbolic"))
        sidebar.add(self.get_sidebar_button("runners-stack", _("Runners"), "applications-utilities-symbolic"))
        sidebar.add(self.get_sidebar_button("services-stack", _("Sources"), "application-x-addon-symbolic"))
        sidebar.add(self.get_sidebar_button("sysinfo-stack", _("Hardware information"), "computer-symbolic"))
        sidebar.add(self.get_sidebar_button("system-stack", _("Global options"), "emblem-system-symbolic"))
        hbox.pack_start(sidebar, False, False, 0)
        self.stack = Gtk.Stack(visible=True)
        self.stack.set_vhomogeneous(False)
        self.stack.set_interpolate_size(True)
        hbox.add(self.stack)
        self.vbox.pack_start(hbox, True, True, 0)
        self.stack.add_named(
            self.build_scrolled_window(PreferencesBox()),
            "prefs-stack"
        )
        self.stack.add_named(
            self.build_scrolled_window(RunnersBox()),
            "runners-stack"
        )
        self.stack.add_named(
            self.build_scrolled_window(ServicesBox()),
            "services-stack"
        )
        self.stack.add_named(
            self.build_scrolled_window(SysInfoBox()),
            "sysinfo-stack"
        )
        self.system_box = SystemBox(self.lutris_config)
        self.system_box.show_all()
        self.stack.add_named(
            self.build_scrolled_window(self.system_box),
            "system-stack"
        )
        self.build_action_area(self.on_save)
        self.action_area.set_margin_bottom(12)
        self.action_area.set_margin_right(12)
        self.action_area.set_margin_left(12)
        self.action_area.set_margin_top(12)

    def on_sidebar_activated(self, _listbox, row):
        if row.get_children()[0].stack_id == "system-stack":
            self.action_area.show_all()
        else:
            self.action_area.hide()
        self.stack.set_visible_child_name(row.get_children()[0].stack_id)

    def get_sidebar_button(self, stack_id, text, icon_name):
        hbox = Gtk.HBox(visible=True)
        hbox.stack_id = stack_id
        hbox.set_margin_top(12)
        hbox.set_margin_bottom(12)
        hbox.set_margin_right(40)

        icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
        icon.show()
        hbox.pack_start(icon, False, False, 6)

        label = Gtk.Label(text, visible=True)
        label.set_alignment(0, 0.5)
        hbox.pack_start(label, False, False, 6)
        return hbox

    def on_save(self, _widget):
        self.lutris_config.save()
        self.destroy()
__init__(self, parent=None) special
Source code in lutris/gui/config/preferences_dialog.py
def __init__(self, parent=None):
    super().__init__(_("Lutris settings"), parent=parent)
    self.set_border_width(0)
    self.set_default_size(1010, 600)
    self.lutris_config = LutrisConfig()

    hbox = Gtk.HBox(visible=True)
    sidebar = Gtk.ListBox(visible=True)
    sidebar.connect("row-selected", self.on_sidebar_activated)
    sidebar.add(self.get_sidebar_button("prefs-stack", _("Interface"), "view-grid-symbolic"))
    sidebar.add(self.get_sidebar_button("runners-stack", _("Runners"), "applications-utilities-symbolic"))
    sidebar.add(self.get_sidebar_button("services-stack", _("Sources"), "application-x-addon-symbolic"))
    sidebar.add(self.get_sidebar_button("sysinfo-stack", _("Hardware information"), "computer-symbolic"))
    sidebar.add(self.get_sidebar_button("system-stack", _("Global options"), "emblem-system-symbolic"))
    hbox.pack_start(sidebar, False, False, 0)
    self.stack = Gtk.Stack(visible=True)
    self.stack.set_vhomogeneous(False)
    self.stack.set_interpolate_size(True)
    hbox.add(self.stack)
    self.vbox.pack_start(hbox, True, True, 0)
    self.stack.add_named(
        self.build_scrolled_window(PreferencesBox()),
        "prefs-stack"
    )
    self.stack.add_named(
        self.build_scrolled_window(RunnersBox()),
        "runners-stack"
    )
    self.stack.add_named(
        self.build_scrolled_window(ServicesBox()),
        "services-stack"
    )
    self.stack.add_named(
        self.build_scrolled_window(SysInfoBox()),
        "sysinfo-stack"
    )
    self.system_box = SystemBox(self.lutris_config)
    self.system_box.show_all()
    self.stack.add_named(
        self.build_scrolled_window(self.system_box),
        "system-stack"
    )
    self.build_action_area(self.on_save)
    self.action_area.set_margin_bottom(12)
    self.action_area.set_margin_right(12)
    self.action_area.set_margin_left(12)
    self.action_area.set_margin_top(12)
get_sidebar_button(self, stack_id, text, icon_name)
Source code in lutris/gui/config/preferences_dialog.py
def get_sidebar_button(self, stack_id, text, icon_name):
    hbox = Gtk.HBox(visible=True)
    hbox.stack_id = stack_id
    hbox.set_margin_top(12)
    hbox.set_margin_bottom(12)
    hbox.set_margin_right(40)

    icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
    icon.show()
    hbox.pack_start(icon, False, False, 6)

    label = Gtk.Label(text, visible=True)
    label.set_alignment(0, 0.5)
    hbox.pack_start(label, False, False, 6)
    return hbox
on_save(self, _widget)

Save game info and destroy widget. Return True if success.

Source code in lutris/gui/config/preferences_dialog.py
def on_save(self, _widget):
    self.lutris_config.save()
    self.destroy()
on_sidebar_activated(self, _listbox, row)
Source code in lutris/gui/config/preferences_dialog.py
def on_sidebar_activated(self, _listbox, row):
    if row.get_children()[0].stack_id == "system-stack":
        self.action_area.show_all()
    else:
        self.action_area.hide()
    self.stack.set_visible_child_name(row.get_children()[0].stack_id)

runner

RunnerConfigDialog (GameDialogCommon)

Runner config edit dialog.

Source code in lutris/gui/config/runner.py
class RunnerConfigDialog(GameDialogCommon):
    """Runner config edit dialog."""

    def __init__(self, runner, parent=None):
        super().__init__(_("Configure %s") % runner.human_name, parent=parent)
        self.runner_name = runner.__class__.__name__
        self.saved = False
        self.lutris_config = LutrisConfig(runner_slug=self.runner_name)
        self.build_notebook()
        self.build_tabs("runner")
        self.build_action_area(self.on_save)
        self.show_all()

    def on_save(self, wigdet, data=None):
        self.lutris_config.save()
        self.destroy()
__init__(self, runner, parent=None) special
Source code in lutris/gui/config/runner.py
def __init__(self, runner, parent=None):
    super().__init__(_("Configure %s") % runner.human_name, parent=parent)
    self.runner_name = runner.__class__.__name__
    self.saved = False
    self.lutris_config = LutrisConfig(runner_slug=self.runner_name)
    self.build_notebook()
    self.build_tabs("runner")
    self.build_action_area(self.on_save)
    self.show_all()
on_save(self, wigdet, data=None)

Save game info and destroy widget. Return True if success.

Source code in lutris/gui/config/runner.py
def on_save(self, wigdet, data=None):
    self.lutris_config.save()
    self.destroy()

runner_box

RunnerBox (Box)
Source code in lutris/gui/config/runner_box.py
class RunnerBox(Gtk.Box):
    __gsignals__ = {
        "runner-installed": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "runner-removed": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, runner_name):
        super().__init__(visible=True)

        self.connect("runner-installed", self.on_runner_installed)
        self.connect("runner-removed", self.on_runner_removed)

        self.set_margin_bottom(12)
        self.set_margin_top(12)
        self.set_margin_left(12)
        self.set_margin_right(12)
        self.runner = runners.import_runner(runner_name)()
        icon = get_icon(self.runner.name, icon_format='pixbuf', size=ICON_SIZE)
        if icon:
            runner_icon = Gtk.Image(visible=True)
            runner_icon.set_from_pixbuf(icon)
        else:
            runner_icon = Gtk.Image.new_from_icon_name("package-x-generic-symbolic", Gtk.IconSize.DND)
            runner_icon.show()
        runner_icon.set_margin_right(12)
        self.pack_start(runner_icon, False, True, 6)

        self.runner_label_box = Gtk.VBox(visible=True)
        self.runner_label_box.set_margin_top(12)

        runner_label = Gtk.Label(visible=True)
        runner_label.set_alignment(0, 0.5)
        runner_label.set_markup("<b>%s</b>" % self.runner.human_name)
        self.runner_label_box.pack_start(runner_label, False, False, 0)

        desc_label = Gtk.Label(visible=True)
        desc_label.set_line_wrap(True)
        desc_label.set_alignment(0, 0.5)
        desc_label.set_text(self.runner.description)
        self.runner_label_box.pack_start(desc_label, False, False, 0)

        self.pack_start(self.runner_label_box, True, True, 0)

        self.configure_button = Gtk.Button.new_from_icon_name("preferences-system-symbolic", Gtk.IconSize.BUTTON)
        self.configure_button.set_margin_right(12)
        self.configure_button.connect("clicked", self.on_configure_clicked)
        self.pack_start(self.configure_button, False, False, 0)
        if not self.runner.is_installed():
            self.runner_label_box.set_sensitive(False)
        self.configure_button.show()
        self.action_alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
        self.action_alignment.show()
        self.action_alignment.add(self.get_action_button())
        self.pack_start(self.action_alignment, False, False, 0)

    def get_action_button(self):
        """Return a install or remove button"""
        if self.runner.multiple_versions:
            _button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
            _button.get_style_context().add_class("circular")
            _button.connect("clicked", self.on_versions_clicked)
        else:
            if self.runner.is_installed():
                _button = Gtk.Button.new_from_icon_name("edit-delete-symbolic", Gtk.IconSize.BUTTON)
                _button.get_style_context().add_class("circular")
                _button.connect("clicked", self.on_remove_clicked)
            else:
                _button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
                _button.get_style_context().add_class("circular")
                _button.connect("clicked", self.on_install_clicked)
        _button.show()
        return _button

    def on_versions_clicked(self, widget):
        RunnerInstallDialog(
            _("Manage %s versions") % self.runner.name,
            None,
            self.runner.name
        )
        # connect a runner-installed signal from the above dialog?

    def on_install_clicked(self, widget):
        """Install a runner."""
        logger.debug("Install of %s requested", self.runner)
        try:
            self.runner.install(downloader=simple_downloader)
        except (
            runners.RunnerInstallationError,
            runners.NonInstallableRunnerError,
        ) as ex:
            logger.error(ex)
            ErrorDialog(ex.message)
            return
        if self.runner.is_installed():
            self.emit("runner-installed")
        else:
            logger.error("Runner failed to install")

    def on_configure_clicked(self, widget):
        window = self.get_toplevel()
        application = window.get_application()
        application.show_window(RunnerConfigDialog, runner=self.runner, parent=window)

    def on_remove_clicked(self, widget):
        dialog = QuestionDialog(
            {
                "title": _("Do you want to uninstall %s?") % self.runner.human_name,
                "question": _("This will remove <b>%s</b> and all associated data." % self.runner.human_name)

            }
        )
        if Gtk.ResponseType.YES == dialog.result:
            self.runner.uninstall()
            self.emit("runner-removed")

    def on_runner_installed(self, widget):
        """Called after the runnner is installed"""
        self.runner_label_box.set_sensitive(True)
        self.action_alignment.get_children()[0].destroy()
        self.action_alignment.add(self.get_action_button())

    def on_runner_removed(self, widget):
        """Called after the runner is removed"""
        self.runner_label_box.set_sensitive(False)
        self.action_alignment.get_children()[0].destroy()
        self.action_alignment.add(self.get_action_button())
__init__(self, runner_name) special
Source code in lutris/gui/config/runner_box.py
def __init__(self, runner_name):
    super().__init__(visible=True)

    self.connect("runner-installed", self.on_runner_installed)
    self.connect("runner-removed", self.on_runner_removed)

    self.set_margin_bottom(12)
    self.set_margin_top(12)
    self.set_margin_left(12)
    self.set_margin_right(12)
    self.runner = runners.import_runner(runner_name)()
    icon = get_icon(self.runner.name, icon_format='pixbuf', size=ICON_SIZE)
    if icon:
        runner_icon = Gtk.Image(visible=True)
        runner_icon.set_from_pixbuf(icon)
    else:
        runner_icon = Gtk.Image.new_from_icon_name("package-x-generic-symbolic", Gtk.IconSize.DND)
        runner_icon.show()
    runner_icon.set_margin_right(12)
    self.pack_start(runner_icon, False, True, 6)

    self.runner_label_box = Gtk.VBox(visible=True)
    self.runner_label_box.set_margin_top(12)

    runner_label = Gtk.Label(visible=True)
    runner_label.set_alignment(0, 0.5)
    runner_label.set_markup("<b>%s</b>" % self.runner.human_name)
    self.runner_label_box.pack_start(runner_label, False, False, 0)

    desc_label = Gtk.Label(visible=True)
    desc_label.set_line_wrap(True)
    desc_label.set_alignment(0, 0.5)
    desc_label.set_text(self.runner.description)
    self.runner_label_box.pack_start(desc_label, False, False, 0)

    self.pack_start(self.runner_label_box, True, True, 0)

    self.configure_button = Gtk.Button.new_from_icon_name("preferences-system-symbolic", Gtk.IconSize.BUTTON)
    self.configure_button.set_margin_right(12)
    self.configure_button.connect("clicked", self.on_configure_clicked)
    self.pack_start(self.configure_button, False, False, 0)
    if not self.runner.is_installed():
        self.runner_label_box.set_sensitive(False)
    self.configure_button.show()
    self.action_alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
    self.action_alignment.show()
    self.action_alignment.add(self.get_action_button())
    self.pack_start(self.action_alignment, False, False, 0)
get_action_button(self)

Return a install or remove button

Source code in lutris/gui/config/runner_box.py
def get_action_button(self):
    """Return a install or remove button"""
    if self.runner.multiple_versions:
        _button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
        _button.get_style_context().add_class("circular")
        _button.connect("clicked", self.on_versions_clicked)
    else:
        if self.runner.is_installed():
            _button = Gtk.Button.new_from_icon_name("edit-delete-symbolic", Gtk.IconSize.BUTTON)
            _button.get_style_context().add_class("circular")
            _button.connect("clicked", self.on_remove_clicked)
        else:
            _button = Gtk.Button.new_from_icon_name("system-software-install-symbolic", Gtk.IconSize.BUTTON)
            _button.get_style_context().add_class("circular")
            _button.connect("clicked", self.on_install_clicked)
    _button.show()
    return _button
on_configure_clicked(self, widget)
Source code in lutris/gui/config/runner_box.py
def on_configure_clicked(self, widget):
    window = self.get_toplevel()
    application = window.get_application()
    application.show_window(RunnerConfigDialog, runner=self.runner, parent=window)
on_install_clicked(self, widget)

Install a runner.

Source code in lutris/gui/config/runner_box.py
def on_install_clicked(self, widget):
    """Install a runner."""
    logger.debug("Install of %s requested", self.runner)
    try:
        self.runner.install(downloader=simple_downloader)
    except (
        runners.RunnerInstallationError,
        runners.NonInstallableRunnerError,
    ) as ex:
        logger.error(ex)
        ErrorDialog(ex.message)
        return
    if self.runner.is_installed():
        self.emit("runner-installed")
    else:
        logger.error("Runner failed to install")
on_remove_clicked(self, widget)
Source code in lutris/gui/config/runner_box.py
def on_remove_clicked(self, widget):
    dialog = QuestionDialog(
        {
            "title": _("Do you want to uninstall %s?") % self.runner.human_name,
            "question": _("This will remove <b>%s</b> and all associated data." % self.runner.human_name)

        }
    )
    if Gtk.ResponseType.YES == dialog.result:
        self.runner.uninstall()
        self.emit("runner-removed")
on_runner_installed(self, widget)

Called after the runnner is installed

Source code in lutris/gui/config/runner_box.py
def on_runner_installed(self, widget):
    """Called after the runnner is installed"""
    self.runner_label_box.set_sensitive(True)
    self.action_alignment.get_children()[0].destroy()
    self.action_alignment.add(self.get_action_button())
on_runner_removed(self, widget)

Called after the runner is removed

Source code in lutris/gui/config/runner_box.py
def on_runner_removed(self, widget):
    """Called after the runner is removed"""
    self.runner_label_box.set_sensitive(False)
    self.action_alignment.get_children()[0].destroy()
    self.action_alignment.add(self.get_action_button())
on_versions_clicked(self, widget)
Source code in lutris/gui/config/runner_box.py
def on_versions_clicked(self, widget):
    RunnerInstallDialog(
        _("Manage %s versions") % self.runner.name,
        None,
        self.runner.name
    )
    # connect a runner-installed signal from the above dialog?

runners_box

Add, remove and configure runners

RunnersBox (BaseConfigBox)

List of all available runners

Source code in lutris/gui/config/runners_box.py
class RunnersBox(BaseConfigBox):
    """List of all available runners"""

    def __init__(self):
        super().__init__()
        self.add(self.get_section_label(_("Add, remove or configure runners")))
        self.add(self.get_description_label(
            _("Runners are programs such as emulators, engines or "
              "translation layers capable of running games.")
        ))
        self.runner_listbox = Gtk.ListBox(visible=True)
        self.pack_start(self.runner_listbox, False, False, 12)
        GLib.idle_add(self.populate_runners)

    def populate_runners(self):
        for runner_name in sorted(runners.__all__):
            list_box_row = Gtk.ListBoxRow(visible=True)
            list_box_row.set_selectable(False)
            list_box_row.set_activatable(False)
            list_box_row.add(RunnerBox(runner_name))
            self.runner_listbox.add(list_box_row)

    @staticmethod
    def on_folder_clicked(_widget):
        open_uri("file://" + settings.RUNNER_DIR)
__init__(self) special
Source code in lutris/gui/config/runners_box.py
def __init__(self):
    super().__init__()
    self.add(self.get_section_label(_("Add, remove or configure runners")))
    self.add(self.get_description_label(
        _("Runners are programs such as emulators, engines or "
          "translation layers capable of running games.")
    ))
    self.runner_listbox = Gtk.ListBox(visible=True)
    self.pack_start(self.runner_listbox, False, False, 12)
    GLib.idle_add(self.populate_runners)
on_folder_clicked(_widget) staticmethod
Source code in lutris/gui/config/runners_box.py
@staticmethod
def on_folder_clicked(_widget):
    open_uri("file://" + settings.RUNNER_DIR)
populate_runners(self)
Source code in lutris/gui/config/runners_box.py
def populate_runners(self):
    for runner_name in sorted(runners.__all__):
        list_box_row = Gtk.ListBoxRow(visible=True)
        list_box_row.set_selectable(False)
        list_box_row.set_activatable(False)
        list_box_row.add(RunnerBox(runner_name))
        self.runner_listbox.add(list_box_row)

services_box

ServicesBox (BaseConfigBox)
Source code in lutris/gui/config/services_box.py
class ServicesBox(BaseConfigBox):
    __gsignals__ = {
        "services-changed": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self):
        super().__init__()
        self.add(self.get_section_label(_("Enable integrations with game sources")))
        self.add(self.get_description_label(
            _("Access your game libraries from various sources. "
              "Changes require a restart to take effect.")
        ))
        self.listbox = Gtk.ListBox(visible=True)
        self.pack_start(self.listbox, False, False, 12)
        GLib.idle_add(self.populate_services)

    def populate_services(self):
        for service_key in SERVICES:
            list_box_row = Gtk.ListBoxRow(visible=True)
            list_box_row.set_selectable(False)
            list_box_row.set_activatable(False)
            list_box_row.add(self._get_service_box(service_key))
            self.listbox.add(list_box_row)

    def _get_service_box(self, service_key):
        box = Gtk.Box(
            spacing=12,
            margin_right=12,
            margin_left=12,
            margin_top=12,
            margin_bottom=12,
            visible=True,
        )
        service = SERVICES[service_key]
        pixbuf = get_icon(service.icon, icon_format="pixbuf", size=ICON_SIZE)
        if pixbuf:
            icon = Gtk.Image(visible=True)
            icon.set_from_pixbuf(pixbuf)
        else:
            icon = Gtk.Image.new_from_icon_name(service.id, Gtk.IconSize.DND)
            icon.show()
        box.pack_start(icon, False, False, 0)
        label = Gtk.Label(service.name, visible=True)
        label.set_alignment(0, 0.5)
        box.pack_start(label, True, True, 0)

        checkbox = Gtk.Switch(visible=True)
        if settings.read_setting(service_key,
                                 section="services").lower() == "true":
            checkbox.set_active(True)
        checkbox.connect("state-set", self._on_service_change, service_key)
        alignment = Gtk.Alignment.new(0.5, 0.5, 0, 0)
        alignment.show()
        alignment.add(checkbox)
        box.pack_start(alignment, False, False, 6)

        return box

    def _on_service_change(self, widget, state, setting_key):
        """Save a setting when an option is toggled"""
        settings.write_setting(setting_key, state, section="services")
        self.emit("services-changed")
__init__(self) special
Source code in lutris/gui/config/services_box.py
def __init__(self):
    super().__init__()
    self.add(self.get_section_label(_("Enable integrations with game sources")))
    self.add(self.get_description_label(
        _("Access your game libraries from various sources. "
          "Changes require a restart to take effect.")
    ))
    self.listbox = Gtk.ListBox(visible=True)
    self.pack_start(self.listbox, False, False, 12)
    GLib.idle_add(self.populate_services)
populate_services(self)
Source code in lutris/gui/config/services_box.py
def populate_services(self):
    for service_key in SERVICES:
        list_box_row = Gtk.ListBoxRow(visible=True)
        list_box_row.set_selectable(False)
        list_box_row.set_activatable(False)
        list_box_row.add(self._get_service_box(service_key))
        self.listbox.add(list_box_row)

sysinfo_box

SysInfoBox (Fixed)
Source code in lutris/gui/config/sysinfo_box.py
class SysInfoBox(Gtk.Fixed):
    settings_options = {
        "hide_client_on_game_start": _("Minimize client when a game is launched"),
        "hide_text_under_icons": _("Hide text under icons"),
        "show_tray_icon": _("Show Tray Icon"),
    }

    def __init__(self):
        super().__init__(visible=True)
        self.set_margin_top(40)
        self.set_margin_right(30)
        self.set_margin_left(30)

        sysinfo_frame = Gtk.Frame(visible=True)
        sysinfo_frame.set_size_request(550, 455)
        scrolled_window = Gtk.ScrolledWindow(visible=True)
        scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)

        sysinfo_view = LogTextView(autoscroll=False)
        sysinfo_view.set_cursor_visible(False)
        scrolled_window.add(sysinfo_view)
        sysinfo_frame.add(scrolled_window)
        sysinfo_str = gather_system_info_str()

        text_buffer = sysinfo_view.get_buffer()
        text_buffer.set_text(sysinfo_str)
        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        self._clipboard_buffer = sysinfo_str

        button_copy = Gtk.Button(_("Copy to clipboard"), visible=True)
        button_copy.connect("clicked", self._copy_text)
        sysinfo_label = Gtk.Label(visible=True)
        sysinfo_label.set_markup(_("<b>System information</b>"))
        self.put(sysinfo_label, 60, 0)
        self.put(sysinfo_frame, 60, 24)
        self.put(button_copy, 60, 486)

    def _copy_text(self, widget):  # pylint: disable=unused-argument
        self.clipboard.set_text(self._clipboard_buffer, -1)
settings_options
__init__(self) special
Source code in lutris/gui/config/sysinfo_box.py
def __init__(self):
    super().__init__(visible=True)
    self.set_margin_top(40)
    self.set_margin_right(30)
    self.set_margin_left(30)

    sysinfo_frame = Gtk.Frame(visible=True)
    sysinfo_frame.set_size_request(550, 455)
    scrolled_window = Gtk.ScrolledWindow(visible=True)
    scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)

    sysinfo_view = LogTextView(autoscroll=False)
    sysinfo_view.set_cursor_visible(False)
    scrolled_window.add(sysinfo_view)
    sysinfo_frame.add(scrolled_window)
    sysinfo_str = gather_system_info_str()

    text_buffer = sysinfo_view.get_buffer()
    text_buffer.set_text(sysinfo_str)
    self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
    self._clipboard_buffer = sysinfo_str

    button_copy = Gtk.Button(_("Copy to clipboard"), visible=True)
    button_copy.connect("clicked", self._copy_text)
    sysinfo_label = Gtk.Label(visible=True)
    sysinfo_label.set_markup(_("<b>System information</b>"))
    self.put(sysinfo_label, 60, 0)
    self.put(sysinfo_frame, 60, 24)
    self.put(button_copy, 60, 486)

dialogs special

Commonly used dialogs

AboutDialog (GtkBuilderDialog)

Source code in lutris/gui/dialogs/__init__.py
class AboutDialog(GtkBuilderDialog):
    glade_file = "about-dialog.ui"
    dialog_object = "about_dialog"

    def initialize(self):  # pylint: disable=arguments-differ
        self.dialog.set_version(settings.VERSION)
dialog_object
glade_file
initialize(self)

Implement further customizations in subclasses

Source code in lutris/gui/dialogs/__init__.py
def initialize(self):  # pylint: disable=arguments-differ
    self.dialog.set_version(settings.VERSION)

ClientLoginDialog (GtkBuilderDialog)

Source code in lutris/gui/dialogs/__init__.py
class ClientLoginDialog(GtkBuilderDialog):
    glade_file = "dialog-lutris-login.ui"
    dialog_object = "lutris-login"
    __gsignals__ = {
        "connected": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
        "cancel": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
    }

    def __init__(self, parent):
        super().__init__(parent=parent)

        self.parent = parent
        self.username_entry = self.builder.get_object("username_entry")
        self.password_entry = self.builder.get_object("password_entry")

        cancel_button = self.builder.get_object("cancel_button")
        cancel_button.connect("clicked", self.on_close)
        connect_button = self.builder.get_object("connect_button")
        connect_button.connect("clicked", self.on_connect)

    def get_credentials(self):
        username = self.username_entry.get_text()
        password = self.password_entry.get_text()
        return username, password

    def on_username_entry_activate(self, widget):  # pylint: disable=unused-argument
        if all(self.get_credentials()):
            self.on_connect(None)
        else:
            self.password_entry.grab_focus()

    def on_password_entry_activate(self, widget):  # pylint: disable=unused-argument
        if all(self.get_credentials()):
            self.on_connect(None)
        else:
            self.username_entry.grab_focus()

    def on_connect(self, widget):  # pylint: disable=unused-argument
        username, password = self.get_credentials()
        token = api.connect(username, password)
        if not token:
            NoticeDialog(_("Login failed"), parent=self.parent)
        else:
            self.emit("connected", username)
            self.dialog.destroy()
dialog_object
glade_file
__init__(self, parent) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, parent):
    super().__init__(parent=parent)

    self.parent = parent
    self.username_entry = self.builder.get_object("username_entry")
    self.password_entry = self.builder.get_object("password_entry")

    cancel_button = self.builder.get_object("cancel_button")
    cancel_button.connect("clicked", self.on_close)
    connect_button = self.builder.get_object("connect_button")
    connect_button.connect("clicked", self.on_connect)
get_credentials(self)
Source code in lutris/gui/dialogs/__init__.py
def get_credentials(self):
    username = self.username_entry.get_text()
    password = self.password_entry.get_text()
    return username, password
on_connect(self, widget)
Source code in lutris/gui/dialogs/__init__.py
def on_connect(self, widget):  # pylint: disable=unused-argument
    username, password = self.get_credentials()
    token = api.connect(username, password)
    if not token:
        NoticeDialog(_("Login failed"), parent=self.parent)
    else:
        self.emit("connected", username)
        self.dialog.destroy()
on_password_entry_activate(self, widget)
Source code in lutris/gui/dialogs/__init__.py
def on_password_entry_activate(self, widget):  # pylint: disable=unused-argument
    if all(self.get_credentials()):
        self.on_connect(None)
    else:
        self.username_entry.grab_focus()
on_username_entry_activate(self, widget)
Source code in lutris/gui/dialogs/__init__.py
def on_username_entry_activate(self, widget):  # pylint: disable=unused-argument
    if all(self.get_credentials()):
        self.on_connect(None)
    else:
        self.password_entry.grab_focus()

Dialog (Dialog)

Source code in lutris/gui/dialogs/__init__.py
class Dialog(Gtk.Dialog):

    def __init__(self, title=None, parent=None, flags=0, buttons=None):
        super().__init__(title, parent, flags, buttons)
        self.set_border_width(10)
        self.connect("delete-event", self.on_destroy)
        self.set_destroy_with_parent(True)

    def on_destroy(self, _widget, _data=None):
        self.destroy()
__init__(self, title=None, parent=None, flags=0, buttons=None) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, title=None, parent=None, flags=0, buttons=None):
    super().__init__(title, parent, flags, buttons)
    self.set_border_width(10)
    self.connect("delete-event", self.on_destroy)
    self.set_destroy_with_parent(True)
on_destroy(self, _widget, _data=None)
Source code in lutris/gui/dialogs/__init__.py
def on_destroy(self, _widget, _data=None):
    self.destroy()

DirectoryDialog

Ask the user to select a directory.

Source code in lutris/gui/dialogs/__init__.py
class DirectoryDialog:

    """Ask the user to select a directory."""

    def __init__(self, message, default_path=None, parent=None):
        self.folder = None
        dialog = Gtk.FileChooserNative.new(
            message,
            parent,
            Gtk.FileChooserAction.SELECT_FOLDER,
            _("_OK"),
            _("_Cancel"),
        )
        if default_path:
            dialog.set_current_folder(default_path)
        self.result = dialog.run()
        if self.result == Gtk.ResponseType.ACCEPT:
            self.folder = dialog.get_filename()
        dialog.destroy()
__init__(self, message, default_path=None, parent=None) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message, default_path=None, parent=None):
    self.folder = None
    dialog = Gtk.FileChooserNative.new(
        message,
        parent,
        Gtk.FileChooserAction.SELECT_FOLDER,
        _("_OK"),
        _("_Cancel"),
    )
    if default_path:
        dialog.set_current_folder(default_path)
    self.result = dialog.run()
    if self.result == Gtk.ResponseType.ACCEPT:
        self.folder = dialog.get_filename()
    dialog.destroy()

DontShowAgainDialog (MessageDialog)

Display a message to the user and offer an option not to display this dialog again.

Source code in lutris/gui/dialogs/__init__.py
class DontShowAgainDialog(Gtk.MessageDialog):

    """Display a message to the user and offer an option not to display this dialog again."""

    def __init__(
        self,
        setting,
        message,
        secondary_message=None,
        parent=None,
        checkbox_message=None,
    ):
        # pylint: disable=no-member
        if settings.read_setting(setting) == "True":
            logger.info("Dialog %s dismissed by user", setting)
            return

        super().__init__(type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, parent=parent)

        self.set_border_width(12)
        self.set_markup("<b>%s</b>" % message)
        if secondary_message:
            self.props.secondary_use_markup = True
            self.props.secondary_text = secondary_message

        if not checkbox_message:
            checkbox_message = _("Do not display this message again.")

        dont_show_checkbutton = Gtk.CheckButton(checkbox_message)
        dont_show_checkbutton.props.halign = Gtk.Align.CENTER
        dont_show_checkbutton.show()

        content_area = self.get_content_area()
        content_area.pack_start(dont_show_checkbutton, False, False, 0)
        self.run()
        if dont_show_checkbutton.get_active():
            settings.write_setting(setting, True)
        self.destroy()
__init__(self, setting, message, secondary_message=None, parent=None, checkbox_message=None) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(
    self,
    setting,
    message,
    secondary_message=None,
    parent=None,
    checkbox_message=None,
):
    # pylint: disable=no-member
    if settings.read_setting(setting) == "True":
        logger.info("Dialog %s dismissed by user", setting)
        return

    super().__init__(type=Gtk.MessageType.WARNING, buttons=Gtk.ButtonsType.OK, parent=parent)

    self.set_border_width(12)
    self.set_markup("<b>%s</b>" % message)
    if secondary_message:
        self.props.secondary_use_markup = True
        self.props.secondary_text = secondary_message

    if not checkbox_message:
        checkbox_message = _("Do not display this message again.")

    dont_show_checkbutton = Gtk.CheckButton(checkbox_message)
    dont_show_checkbutton.props.halign = Gtk.Align.CENTER
    dont_show_checkbutton.show()

    content_area = self.get_content_area()
    content_area.pack_start(dont_show_checkbutton, False, False, 0)
    self.run()
    if dont_show_checkbutton.get_active():
        settings.write_setting(setting, True)
    self.destroy()

ErrorDialog (MessageDialog)

Display an error message.

Source code in lutris/gui/dialogs/__init__.py
class ErrorDialog(Gtk.MessageDialog):
    """Display an error message."""

    def __init__(self, message, secondary=None, parent=None):
        super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
        # Gtk doesn't wrap long labels containing no space correctly
        # the length of the message is limited to avoid display issues
        self.set_markup(message[:256])
        if secondary:
            self.format_secondary_text(secondary[:256])
        self.run()
        self.destroy()
__init__(self, message, secondary=None, parent=None) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message, secondary=None, parent=None):
    super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
    # Gtk doesn't wrap long labels containing no space correctly
    # the length of the message is limited to avoid display issues
    self.set_markup(message[:256])
    if secondary:
        self.format_secondary_text(secondary[:256])
    self.run()
    self.destroy()

FileDialog

Ask the user to select a file.

Source code in lutris/gui/dialogs/__init__.py
class FileDialog:

    """Ask the user to select a file."""

    def __init__(self, message=None, default_path=None, mode="open"):
        self.filename = None
        if not message:
            message = _("Please choose a file")
        if mode == "save":
            action = Gtk.FileChooserAction.SAVE
        else:
            action = Gtk.FileChooserAction.OPEN
        dialog = Gtk.FileChooserNative.new(
            message,
            None,
            action,
            _("_OK"),
            _("_Cancel"),
        )
        if default_path and os.path.exists(default_path):
            dialog.set_current_folder(default_path)
        dialog.set_local_only(False)
        response = dialog.run()
        if response == Gtk.ResponseType.ACCEPT:
            self.filename = dialog.get_filename()

        dialog.destroy()
__init__(self, message=None, default_path=None, mode='open') special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message=None, default_path=None, mode="open"):
    self.filename = None
    if not message:
        message = _("Please choose a file")
    if mode == "save":
        action = Gtk.FileChooserAction.SAVE
    else:
        action = Gtk.FileChooserAction.OPEN
    dialog = Gtk.FileChooserNative.new(
        message,
        None,
        action,
        _("_OK"),
        _("_Cancel"),
    )
    if default_path and os.path.exists(default_path):
        dialog.set_current_folder(default_path)
    dialog.set_local_only(False)
    response = dialog.run()
    if response == Gtk.ResponseType.ACCEPT:
        self.filename = dialog.get_filename()

    dialog.destroy()

GtkBuilderDialog (Object)

Source code in lutris/gui/dialogs/__init__.py
class GtkBuilderDialog(GObject.Object):
    dialog_object = NotImplemented

    __gsignals__ = {
        "destroy": (GObject.SignalFlags.RUN_LAST, None, ()),
    }

    def __init__(self, parent=None, **kwargs):
        # pylint: disable=no-member
        super().__init__()
        ui_filename = os.path.join(datapath.get(), "ui", self.glade_file)
        if not os.path.exists(ui_filename):
            raise ValueError("ui file does not exists: %s" % ui_filename)

        self.builder = Gtk.Builder()
        self.builder.add_from_file(ui_filename)
        self.dialog = self.builder.get_object(self.dialog_object)

        self.builder.connect_signals(self)
        if parent:
            self.dialog.set_transient_for(parent)
        self.dialog.show_all()
        self.dialog.connect("delete-event", self.on_close)
        self.initialize(**kwargs)

    def initialize(self, **kwargs):
        """Implement further customizations in subclasses"""

    def present(self):
        self.dialog.present()

    def on_close(self, *args):  # pylint: disable=unused-argument
        """Propagate the destroy event after closing the dialog"""
        self.dialog.destroy()
        self.emit("destroy")

    def on_response(self, widget, response):  # pylint: disable=unused-argument
        if response == Gtk.ResponseType.DELETE_EVENT:
            try:
                self.dialog.hide()
            except AttributeError:
                pass
dialog_object
__init__(self, parent=None, **kwargs) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, parent=None, **kwargs):
    # pylint: disable=no-member
    super().__init__()
    ui_filename = os.path.join(datapath.get(), "ui", self.glade_file)
    if not os.path.exists(ui_filename):
        raise ValueError("ui file does not exists: %s" % ui_filename)

    self.builder = Gtk.Builder()
    self.builder.add_from_file(ui_filename)
    self.dialog = self.builder.get_object(self.dialog_object)

    self.builder.connect_signals(self)
    if parent:
        self.dialog.set_transient_for(parent)
    self.dialog.show_all()
    self.dialog.connect("delete-event", self.on_close)
    self.initialize(**kwargs)
initialize(self, **kwargs)

Implement further customizations in subclasses

Source code in lutris/gui/dialogs/__init__.py
def initialize(self, **kwargs):
    """Implement further customizations in subclasses"""
on_close(self, *args)

Propagate the destroy event after closing the dialog

Source code in lutris/gui/dialogs/__init__.py
def on_close(self, *args):  # pylint: disable=unused-argument
    """Propagate the destroy event after closing the dialog"""
    self.dialog.destroy()
    self.emit("destroy")
on_response(self, widget, response)
Source code in lutris/gui/dialogs/__init__.py
def on_response(self, widget, response):  # pylint: disable=unused-argument
    if response == Gtk.ResponseType.DELETE_EVENT:
        try:
            self.dialog.hide()
        except AttributeError:
            pass
present(self)
Source code in lutris/gui/dialogs/__init__.py
def present(self):
    self.dialog.present()

InstallOrPlayDialog (Dialog)

Source code in lutris/gui/dialogs/__init__.py
class InstallOrPlayDialog(Gtk.Dialog):

    def __init__(self, game_name):
        Gtk.Dialog.__init__(self, _("%s is already installed") % game_name)
        self.connect("delete-event", lambda *x: self.destroy())
        self.action = "play"
        self.action_confirmed = False

        self.set_size_request(320, 120)
        self.set_border_width(12)
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
        self.get_content_area().add(vbox)
        play_button = Gtk.RadioButton.new_with_label_from_widget(None, _("Launch game"))
        play_button.connect("toggled", self.on_button_toggled, "play")
        vbox.pack_start(play_button, False, False, 0)
        install_button = Gtk.RadioButton.new_from_widget(play_button)
        install_button.set_label(_("Install the game again"))
        install_button.connect("toggled", self.on_button_toggled, "install")
        vbox.pack_start(install_button, False, False, 0)

        confirm_button = Gtk.Button(_("OK"))
        confirm_button.connect("clicked", self.on_confirm)
        vbox.pack_start(confirm_button, False, False, 0)

        self.show_all()
        self.run()

    def on_button_toggled(self, button, action):  # pylint: disable=unused-argument
        logger.debug("Action set to %s", action)
        self.action = action

    def on_confirm(self, button):  # pylint: disable=unused-argument
        logger.debug("Action %s confirmed", self.action)
        self.action_confirmed = True
        self.destroy()
__init__(self, game_name) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, game_name):
    Gtk.Dialog.__init__(self, _("%s is already installed") % game_name)
    self.connect("delete-event", lambda *x: self.destroy())
    self.action = "play"
    self.action_confirmed = False

    self.set_size_request(320, 120)
    self.set_border_width(12)
    vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
    self.get_content_area().add(vbox)
    play_button = Gtk.RadioButton.new_with_label_from_widget(None, _("Launch game"))
    play_button.connect("toggled", self.on_button_toggled, "play")
    vbox.pack_start(play_button, False, False, 0)
    install_button = Gtk.RadioButton.new_from_widget(play_button)
    install_button.set_label(_("Install the game again"))
    install_button.connect("toggled", self.on_button_toggled, "install")
    vbox.pack_start(install_button, False, False, 0)

    confirm_button = Gtk.Button(_("OK"))
    confirm_button.connect("clicked", self.on_confirm)
    vbox.pack_start(confirm_button, False, False, 0)

    self.show_all()
    self.run()
on_button_toggled(self, button, action)
Source code in lutris/gui/dialogs/__init__.py
def on_button_toggled(self, button, action):  # pylint: disable=unused-argument
    logger.debug("Action set to %s", action)
    self.action = action
on_confirm(self, button)
Source code in lutris/gui/dialogs/__init__.py
def on_confirm(self, button):  # pylint: disable=unused-argument
    logger.debug("Action %s confirmed", self.action)
    self.action_confirmed = True
    self.destroy()

InstallerSourceDialog (Dialog)

Show install script source

Source code in lutris/gui/dialogs/__init__.py
class InstallerSourceDialog(Gtk.Dialog):

    """Show install script source"""

    def __init__(self, code, name, parent):
        Gtk.Dialog.__init__(self, _("Install script for {}").format(name), parent=parent)
        self.set_size_request(500, 350)
        self.set_border_width(0)

        self.scrolled_window = Gtk.ScrolledWindow()
        self.scrolled_window.set_hexpand(True)
        self.scrolled_window.set_vexpand(True)

        source_buffer = Gtk.TextBuffer()
        source_buffer.set_text(code)

        source_box = LogTextView(source_buffer, autoscroll=False)

        self.get_content_area().add(self.scrolled_window)
        self.scrolled_window.add(source_box)

        close_button = Gtk.Button(_("OK"))
        close_button.connect("clicked", self.on_close)
        self.get_content_area().add(close_button)

        self.show_all()

    def on_close(self, *args):  # pylint: disable=unused-argument
        self.destroy()
__init__(self, code, name, parent) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, code, name, parent):
    Gtk.Dialog.__init__(self, _("Install script for {}").format(name), parent=parent)
    self.set_size_request(500, 350)
    self.set_border_width(0)

    self.scrolled_window = Gtk.ScrolledWindow()
    self.scrolled_window.set_hexpand(True)
    self.scrolled_window.set_vexpand(True)

    source_buffer = Gtk.TextBuffer()
    source_buffer.set_text(code)

    source_box = LogTextView(source_buffer, autoscroll=False)

    self.get_content_area().add(self.scrolled_window)
    self.scrolled_window.add(source_box)

    close_button = Gtk.Button(_("OK"))
    close_button.connect("clicked", self.on_close)
    self.get_content_area().add(close_button)

    self.show_all()
on_close(self, *args)
Source code in lutris/gui/dialogs/__init__.py
def on_close(self, *args):  # pylint: disable=unused-argument
    self.destroy()

LaunchConfigSelectDialog (Dialog)

Source code in lutris/gui/dialogs/__init__.py
class LaunchConfigSelectDialog(Gtk.Dialog):
    def __init__(self, game, configs):
        Gtk.Dialog.__init__(self, _("Select game to launch"))
        self.connect("delete-event", lambda *x: self.destroy())
        self.config_index = 0
        self.set_size_request(320, 120)
        self.set_border_width(12)
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
        self.get_content_area().add(vbox)

        primary_game_radio = Gtk.RadioButton.new_with_label_from_widget(None, game.name)
        primary_game_radio.connect("toggled", self.on_button_toggled, 0)
        vbox.pack_start(primary_game_radio, False, False, 0)
        for i, config in enumerate(configs):
            _button = Gtk.RadioButton.new_from_widget(primary_game_radio)
            _button.set_label(config["name"])
            _button.connect("toggled", self.on_button_toggled, i + 1)
            vbox.pack_start(_button, False, False, 0)

        confirm_button = Gtk.Button(_("OK"))
        confirm_button.connect("clicked", self.on_confirm)
        vbox.pack_start(confirm_button, False, False, 0)

        self.show_all()
        self.run()

    def on_button_toggled(self, _button, index):
        self.config_index = index

    def on_confirm(self, _button):
        self.destroy()
__init__(self, game, configs) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, game, configs):
    Gtk.Dialog.__init__(self, _("Select game to launch"))
    self.connect("delete-event", lambda *x: self.destroy())
    self.config_index = 0
    self.set_size_request(320, 120)
    self.set_border_width(12)
    vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 6)
    self.get_content_area().add(vbox)

    primary_game_radio = Gtk.RadioButton.new_with_label_from_widget(None, game.name)
    primary_game_radio.connect("toggled", self.on_button_toggled, 0)
    vbox.pack_start(primary_game_radio, False, False, 0)
    for i, config in enumerate(configs):
        _button = Gtk.RadioButton.new_from_widget(primary_game_radio)
        _button.set_label(config["name"])
        _button.connect("toggled", self.on_button_toggled, i + 1)
        vbox.pack_start(_button, False, False, 0)

    confirm_button = Gtk.Button(_("OK"))
    confirm_button.connect("clicked", self.on_confirm)
    vbox.pack_start(confirm_button, False, False, 0)

    self.show_all()
    self.run()
on_button_toggled(self, _button, index)
Source code in lutris/gui/dialogs/__init__.py
def on_button_toggled(self, _button, index):
    self.config_index = index
on_confirm(self, _button)
Source code in lutris/gui/dialogs/__init__.py
def on_confirm(self, _button):
    self.destroy()

LutrisInitDialog (Dialog)

Source code in lutris/gui/dialogs/__init__.py
class LutrisInitDialog(Gtk.Dialog):

    def __init__(self, init_lutris):
        super().__init__()
        self.set_size_request(320, 60)
        self.set_border_width(24)
        self.set_decorated(False)
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
        label = Gtk.Label(_("Checking for runtime updates, please wait…"))
        vbox.add(label)
        self.progress = Gtk.ProgressBar(visible=True)
        self.progress.set_pulse_step(0.1)
        vbox.add(self.progress)
        self.get_content_area().add(vbox)
        self.progress_timeout = GLib.timeout_add(125, self.show_progress)
        self.show_all()
        self.connect("destroy", self.on_destroy)
        AsyncCall(self.initialize, self.init_cb, init_lutris)

    def show_progress(self):
        self.progress.pulse()
        return True

    def initialize(self, init_lutris, *args):
        init_lutris()

    def init_cb(self, _result, error):
        if error:
            ErrorDialog(str(error))
        self.destroy()

    def on_destroy(self, window):
        GLib.source_remove(self.progress_timeout)
        return True
__init__(self, init_lutris) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, init_lutris):
    super().__init__()
    self.set_size_request(320, 60)
    self.set_border_width(24)
    self.set_decorated(False)
    vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
    label = Gtk.Label(_("Checking for runtime updates, please wait…"))
    vbox.add(label)
    self.progress = Gtk.ProgressBar(visible=True)
    self.progress.set_pulse_step(0.1)
    vbox.add(self.progress)
    self.get_content_area().add(vbox)
    self.progress_timeout = GLib.timeout_add(125, self.show_progress)
    self.show_all()
    self.connect("destroy", self.on_destroy)
    AsyncCall(self.initialize, self.init_cb, init_lutris)
init_cb(self, _result, error)
Source code in lutris/gui/dialogs/__init__.py
def init_cb(self, _result, error):
    if error:
        ErrorDialog(str(error))
    self.destroy()
initialize(self, init_lutris, *args)
Source code in lutris/gui/dialogs/__init__.py
def initialize(self, init_lutris, *args):
    init_lutris()
on_destroy(self, window)
Source code in lutris/gui/dialogs/__init__.py
def on_destroy(self, window):
    GLib.source_remove(self.progress_timeout)
    return True
show_progress(self)
Source code in lutris/gui/dialogs/__init__.py
def show_progress(self):
    self.progress.pulse()
    return True

MoveDialog (Dialog)

Source code in lutris/gui/dialogs/__init__.py
class MoveDialog(Gtk.Dialog):
    __gsignals__ = {
        "game-moved": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, game, destination):
        super().__init__()

        self.game = game
        self.destination = destination
        self.new_directory = None

        self.set_size_request(320, 60)
        self.set_border_width(24)
        self.set_decorated(False)
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
        label = Gtk.Label(_("Moving %s to %s..." % (game, destination)))
        vbox.add(label)
        self.progress = Gtk.ProgressBar(visible=True)
        self.progress.set_pulse_step(0.1)
        vbox.add(self.progress)
        self.get_content_area().add(vbox)
        GLib.timeout_add(125, self.show_progress)
        self.show_all()

    def move(self):
        AsyncCall(self._move_game, self.on_game_moved)

    def show_progress(self):
        self.progress.pulse()
        return True

    def _move_game(self):
        self.new_directory = self.game.move(self.destination)

    def on_game_moved(self, _result, error):
        if error:
            ErrorDialog(str(error))
        self.emit("game-moved")
        self.destroy()
__init__(self, game, destination) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, game, destination):
    super().__init__()

    self.game = game
    self.destination = destination
    self.new_directory = None

    self.set_size_request(320, 60)
    self.set_border_width(24)
    self.set_decorated(False)
    vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 12)
    label = Gtk.Label(_("Moving %s to %s..." % (game, destination)))
    vbox.add(label)
    self.progress = Gtk.ProgressBar(visible=True)
    self.progress.set_pulse_step(0.1)
    vbox.add(self.progress)
    self.get_content_area().add(vbox)
    GLib.timeout_add(125, self.show_progress)
    self.show_all()
move(self)

move(self, x:int, y:int)

Source code in lutris/gui/dialogs/__init__.py
def move(self):
    AsyncCall(self._move_game, self.on_game_moved)
on_game_moved(self, _result, error)
Source code in lutris/gui/dialogs/__init__.py
def on_game_moved(self, _result, error):
    if error:
        ErrorDialog(str(error))
    self.emit("game-moved")
    self.destroy()
show_progress(self)
Source code in lutris/gui/dialogs/__init__.py
def show_progress(self):
    self.progress.pulse()
    return True

NoticeDialog (MessageDialog)

Display a message to the user.

Source code in lutris/gui/dialogs/__init__.py
class NoticeDialog(Gtk.MessageDialog):

    """Display a message to the user."""

    def __init__(self, message, parent=None):
        super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
        self.set_markup(message)
        self.run()
        self.destroy()
__init__(self, message, parent=None) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, message, parent=None):
    super().__init__(buttons=Gtk.ButtonsType.OK, parent=parent)
    self.set_markup(message)
    self.run()
    self.destroy()

QuestionDialog (MessageDialog)

Ask the user a question.

Source code in lutris/gui/dialogs/__init__.py
class QuestionDialog(Gtk.MessageDialog):

    """Ask the user a question."""

    YES = Gtk.ResponseType.YES
    NO = Gtk.ResponseType.NO

    def __init__(self, dialog_settings):
        super().__init__(message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO)
        self.set_markup(dialog_settings["question"])
        self.set_title(dialog_settings["title"])
        if "parent" in dialog_settings:
            self.set_transient_for(dialog_settings["parent"])
        if "widgets" in dialog_settings:
            for widget in dialog_settings["widgets"]:
                self.get_message_area().add(widget)
        self.result = self.run()
        self.destroy()
NO
YES
__init__(self, dialog_settings) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, dialog_settings):
    super().__init__(message_type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.YES_NO)
    self.set_markup(dialog_settings["question"])
    self.set_title(dialog_settings["title"])
    if "parent" in dialog_settings:
        self.set_transient_for(dialog_settings["parent"])
    if "widgets" in dialog_settings:
        for widget in dialog_settings["widgets"]:
            self.get_message_area().add(widget)
    self.result = self.run()
    self.destroy()

WineNotInstalledWarning (DontShowAgainDialog)

Display a warning if Wine is not detected on the system

Source code in lutris/gui/dialogs/__init__.py
class WineNotInstalledWarning(DontShowAgainDialog):

    """Display a warning if Wine is not detected on the system"""

    def __init__(self, parent=None):
        super().__init__(
            "hide-wine-systemwide-install-warning",
            _("Wine is not installed on your system."),
            secondary_message=_(
                "Having Wine installed on your system guarantees that "
                "Wine builds from Lutris will have all required dependencies.\n\nPlease "
                "follow the instructions given in the <a "
                "href='https://github.com/lutris/lutris/wiki/Wine-Dependencies'>Lutris Wiki</a> to "
                "install Wine."
            ),
            parent=parent,
        )
__init__(self, parent=None) special
Source code in lutris/gui/dialogs/__init__.py
def __init__(self, parent=None):
    super().__init__(
        "hide-wine-systemwide-install-warning",
        _("Wine is not installed on your system."),
        secondary_message=_(
            "Having Wine installed on your system guarantees that "
            "Wine builds from Lutris will have all required dependencies.\n\nPlease "
            "follow the instructions given in the <a "
            "href='https://github.com/lutris/lutris/wiki/Wine-Dependencies'>Lutris Wiki</a> to "
            "install Wine."
        ),
        parent=parent,
    )

cache

CacheConfigurationDialog (Dialog)
Source code in lutris/gui/dialogs/cache.py
class CacheConfigurationDialog(Gtk.Dialog):
    def __init__(self):
        Gtk.Dialog.__init__(self, _("Cache configuration"))
        self.timer_id = None
        self.set_size_request(480, 150)
        self.set_border_width(12)

        self.get_content_area().add(self.get_cache_config())
        self.show_all()

    def get_cache_config(self):
        """Return the widgets for the cache configuration"""
        prefs_box = Gtk.VBox()

        box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
        label = Gtk.Label(_("Cache path"))
        box.pack_start(label, False, False, 0)
        cache_path = get_cache_path()
        path_chooser = FileChooserEntry(
            title=_("Set the folder for the cache path"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=cache_path
        )
        path_chooser.entry.connect("changed", self._on_cache_path_set)
        box.pack_start(path_chooser, True, True, 0)

        prefs_box.pack_start(box, False, False, 6)
        cache_help_label = Gtk.Label(visible=True)
        cache_help_label.set_size_request(400, -1)
        cache_help_label.set_markup(_(
            "If provided, this location will be used by installers to cache "
            "downloaded files locally for future re-use. \nIf left empty, the "
            "installer files are discarded after the install completion."
        ))
        prefs_box.pack_start(cache_help_label, False, False, 6)
        return prefs_box

    def _on_cache_path_set(self, entry):
        if self.timer_id:
            GLib.source_remove(self.timer_id)
        self.timer_id = GLib.timeout_add(1000, self.save_cache_setting, entry.get_text())

    def save_cache_setting(self, value):
        save_cache_path(value)
        GLib.source_remove(self.timer_id)
        self.timer_id = None
        return False
__init__(self) special
Source code in lutris/gui/dialogs/cache.py
def __init__(self):
    Gtk.Dialog.__init__(self, _("Cache configuration"))
    self.timer_id = None
    self.set_size_request(480, 150)
    self.set_border_width(12)

    self.get_content_area().add(self.get_cache_config())
    self.show_all()
get_cache_config(self)

Return the widgets for the cache configuration

Source code in lutris/gui/dialogs/cache.py
def get_cache_config(self):
    """Return the widgets for the cache configuration"""
    prefs_box = Gtk.VBox()

    box = Gtk.Box(spacing=12, margin_right=12, margin_left=12)
    label = Gtk.Label(_("Cache path"))
    box.pack_start(label, False, False, 0)
    cache_path = get_cache_path()
    path_chooser = FileChooserEntry(
        title=_("Set the folder for the cache path"), action=Gtk.FileChooserAction.SELECT_FOLDER, path=cache_path
    )
    path_chooser.entry.connect("changed", self._on_cache_path_set)
    box.pack_start(path_chooser, True, True, 0)

    prefs_box.pack_start(box, False, False, 6)
    cache_help_label = Gtk.Label(visible=True)
    cache_help_label.set_size_request(400, -1)
    cache_help_label.set_markup(_(
        "If provided, this location will be used by installers to cache "
        "downloaded files locally for future re-use. \nIf left empty, the "
        "installer files are discarded after the install completion."
    ))
    prefs_box.pack_start(cache_help_label, False, False, 6)
    return prefs_box
save_cache_setting(self, value)
Source code in lutris/gui/dialogs/cache.py
def save_cache_setting(self, value):
    save_cache_path(value)
    GLib.source_remove(self.timer_id)
    self.timer_id = None
    return False

download

DownloadDialog (Dialog)

Dialog showing a download in progress.

Source code in lutris/gui/dialogs/download.py
class DownloadDialog(Gtk.Dialog):
    """Dialog showing a download in progress."""

    def __init__(self, url=None, dest=None, title=None, label=None, downloader=None):
        Gtk.Dialog.__init__(self, title or _("Downloading file"))
        self.set_size_request(485, 104)
        self.set_border_width(12)
        params = {"url": url, "dest": dest, "title": label or _("Downloading %s") % url}
        self.dialog_progress_box = DownloadProgressBox(params, downloader=downloader)

        self.dialog_progress_box.connect("complete", self.download_complete)
        self.dialog_progress_box.connect("cancel", self.download_cancelled)
        self.connect("response", self.on_response)

        self.get_content_area().add(self.dialog_progress_box)
        self.show_all()
        self.dialog_progress_box.start()

    def download_complete(self, _widget, _data):
        self.response(Gtk.ResponseType.OK)
        self.destroy()

    def download_cancelled(self, _widget, data):
        self.response(Gtk.ResponseType.CANCEL)
        self.destroy()

    def on_response(self, _dialog, response):
        if response == Gtk.ResponseType.DELETE_EVENT:
            self.dialog_progress_box.downloader.cancel()
            self.destroy()
__init__(self, url=None, dest=None, title=None, label=None, downloader=None) special
Source code in lutris/gui/dialogs/download.py
def __init__(self, url=None, dest=None, title=None, label=None, downloader=None):
    Gtk.Dialog.__init__(self, title or _("Downloading file"))
    self.set_size_request(485, 104)
    self.set_border_width(12)
    params = {"url": url, "dest": dest, "title": label or _("Downloading %s") % url}
    self.dialog_progress_box = DownloadProgressBox(params, downloader=downloader)

    self.dialog_progress_box.connect("complete", self.download_complete)
    self.dialog_progress_box.connect("cancel", self.download_cancelled)
    self.connect("response", self.on_response)

    self.get_content_area().add(self.dialog_progress_box)
    self.show_all()
    self.dialog_progress_box.start()
download_cancelled(self, _widget, data)
Source code in lutris/gui/dialogs/download.py
def download_cancelled(self, _widget, data):
    self.response(Gtk.ResponseType.CANCEL)
    self.destroy()
download_complete(self, _widget, _data)
Source code in lutris/gui/dialogs/download.py
def download_complete(self, _widget, _data):
    self.response(Gtk.ResponseType.OK)
    self.destroy()
on_response(self, _dialog, response)
Source code in lutris/gui/dialogs/download.py
def on_response(self, _dialog, response):
    if response == Gtk.ResponseType.DELETE_EVENT:
        self.dialog_progress_box.downloader.cancel()
        self.destroy()
simple_downloader(url, destination, callback, callback_args=None)

Basic downloader with a DownloadDialog

Source code in lutris/gui/dialogs/download.py
def simple_downloader(url, destination, callback, callback_args=None):
    """Basic downloader with a DownloadDialog"""
    if not callback_args:
        callback_args = {}
    dialog = DownloadDialog(url, destination)
    dialog.run()
    return callback(**callback_args)

issue

GUI dialog for reporting issues

IssueReportWindow (BaseApplicationWindow)

Window for collecting and sending issue reports

Source code in lutris/gui/dialogs/issue.py
class IssueReportWindow(BaseApplicationWindow):

    """Window for collecting and sending issue reports"""

    def __init__(self, application):
        super().__init__(application)

        self.title_label = Gtk.Label(visible=True)
        self.vbox.add(self.title_label)

        title_label = Gtk.Label()
        title_label.set_markup(_("<b>Submit an issue</b>"))
        self.vbox.add(title_label)
        self.vbox.add(Gtk.HSeparator())

        issue_entry_label = Gtk.Label(_(
            "Describe the problem you're having in the text box below. "
            "This information will be sent the Lutris team along with your system information. "
            "You can also save this information locally if you are offline."
        ))
        issue_entry_label.set_max_width_chars(80)
        issue_entry_label.set_property("wrap", True)
        self.vbox.add(issue_entry_label)

        self.textview = Gtk.TextView()
        self.textview.set_pixels_above_lines(12)
        self.textview.set_pixels_below_lines(12)
        self.textview.set_left_margin(12)
        self.textview.set_right_margin(12)
        self.vbox.pack_start(self.textview, True, True, 0)

        self.action_buttons = Gtk.Box(spacing=6)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        self.vbox.pack_start(action_buttons_alignment, False, True, 0)

        cancel_button = self.get_action_button(_("C_ancel"), handler=self.on_destroy)
        self.action_buttons.add(cancel_button)

        save_button = self.get_action_button(_("_Save"), handler=self.on_save)
        self.action_buttons.add(save_button)

        self.show_all()

    def get_issue_info(self):
        buffer = self.textview.get_buffer()
        return {
            'comment': buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
            'system': gather_system_info()
        }

    def on_save(self, _button):
        """Signal handler for the save button"""

        save_dialog = Gtk.FileChooserNative.new(
            _("Select a location to save the issue"),
            self,
            Gtk.FileChooserAction.SELECT_FOLDER,
            _("_OK"),
            _("_Cancel"),
        )
        save_dialog.connect("response", self.on_folder_selected, save_dialog)
        save_dialog.show()

    def on_folder_selected(self, dialog, response, _dialog):
        if response != Gtk.ResponseType.ACCEPT:
            return
        target_path = dialog.get_filename()
        if not target_path:
            return
        issue_path = os.path.join(target_path, "lutris-issue-report.json")
        issue_info = self.get_issue_info()
        with open(issue_path, "w", encoding='utf-8') as issue_file:
            json.dump(issue_info, issue_file, indent=2)
        dialog.destroy()
        NoticeDialog(_("Issue saved in %s") % issue_path)
        self.destroy()
__init__(self, application) special
Source code in lutris/gui/dialogs/issue.py
def __init__(self, application):
    super().__init__(application)

    self.title_label = Gtk.Label(visible=True)
    self.vbox.add(self.title_label)

    title_label = Gtk.Label()
    title_label.set_markup(_("<b>Submit an issue</b>"))
    self.vbox.add(title_label)
    self.vbox.add(Gtk.HSeparator())

    issue_entry_label = Gtk.Label(_(
        "Describe the problem you're having in the text box below. "
        "This information will be sent the Lutris team along with your system information. "
        "You can also save this information locally if you are offline."
    ))
    issue_entry_label.set_max_width_chars(80)
    issue_entry_label.set_property("wrap", True)
    self.vbox.add(issue_entry_label)

    self.textview = Gtk.TextView()
    self.textview.set_pixels_above_lines(12)
    self.textview.set_pixels_below_lines(12)
    self.textview.set_left_margin(12)
    self.textview.set_right_margin(12)
    self.vbox.pack_start(self.textview, True, True, 0)

    self.action_buttons = Gtk.Box(spacing=6)
    action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
    action_buttons_alignment.add(self.action_buttons)
    self.vbox.pack_start(action_buttons_alignment, False, True, 0)

    cancel_button = self.get_action_button(_("C_ancel"), handler=self.on_destroy)
    self.action_buttons.add(cancel_button)

    save_button = self.get_action_button(_("_Save"), handler=self.on_save)
    self.action_buttons.add(save_button)

    self.show_all()
get_issue_info(self)
Source code in lutris/gui/dialogs/issue.py
def get_issue_info(self):
    buffer = self.textview.get_buffer()
    return {
        'comment': buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
        'system': gather_system_info()
    }
on_folder_selected(self, dialog, response, _dialog)
Source code in lutris/gui/dialogs/issue.py
def on_folder_selected(self, dialog, response, _dialog):
    if response != Gtk.ResponseType.ACCEPT:
        return
    target_path = dialog.get_filename()
    if not target_path:
        return
    issue_path = os.path.join(target_path, "lutris-issue-report.json")
    issue_info = self.get_issue_info()
    with open(issue_path, "w", encoding='utf-8') as issue_file:
        json.dump(issue_info, issue_file, indent=2)
    dialog.destroy()
    NoticeDialog(_("Issue saved in %s") % issue_path)
    self.destroy()
on_save(self, _button)

Signal handler for the save button

Source code in lutris/gui/dialogs/issue.py
def on_save(self, _button):
    """Signal handler for the save button"""

    save_dialog = Gtk.FileChooserNative.new(
        _("Select a location to save the issue"),
        self,
        Gtk.FileChooserAction.SELECT_FOLDER,
        _("_OK"),
        _("_Cancel"),
    )
    save_dialog.connect("response", self.on_folder_selected, save_dialog)
    save_dialog.show()

log

Window to show game logs

LogWindow (Object)
Source code in lutris/gui/dialogs/log.py
class LogWindow(GObject.Object):

    def __init__(self, title=None, buffer=None, application=None):
        super().__init__()
        ui_filename = os.path.join(datapath.get(), "ui/log-window.ui")
        builder = Gtk.Builder()
        builder.add_from_file(ui_filename)
        builder.connect_signals(self)
        window = builder.get_object("log_window")
        window.set_title(title)
        self.title = title

        self.buffer = buffer
        self.logtextview = LogTextView(self.buffer)

        scrolled_window = builder.get_object("scrolled_window")
        scrolled_window.add(self.logtextview)

        self.search_entry = builder.get_object("search_entry")
        self.search_entry.connect("search-changed", self.logtextview.find_first)
        self.search_entry.connect("next-match", self.logtextview.find_next)
        self.search_entry.connect("previous-match", self.logtextview.find_previous)

        save_button = builder.get_object("save_button")
        save_button.connect("clicked", self.on_save_clicked)

        window.connect("key-press-event", self.on_key_press_event)
        window.show_all()

    def on_key_press_event(self, widget, event):
        shift = (event.state & Gdk.ModifierType.SHIFT_MASK)
        if event.keyval == Gdk.KEY_Return:
            if shift:
                self.search_entry.emit("previous-match")
            else:
                self.search_entry.emit("next-match")

    def on_save_clicked(self, _button):
        """Handler to save log to a file"""
        now = datetime.now()
        log_filename = "%s (%s).log" % (self.title, now.strftime("%Y-%m-%d-%H-%M"))
        file_dialog = FileDialog(
            message="Save the logs to...",
            default_path=os.path.expanduser("~/%s" % log_filename),
            mode="save"
        )
        log_path = file_dialog.filename
        if not log_path:
            return

        text = self.buffer.get_text(
            self.buffer.get_start_iter(),
            self.buffer.get_end_iter(),
            True
        )
        with open(log_path, "w", encoding='utf-8') as log_file:
            log_file.write(text)
__init__(self, title=None, buffer=None, application=None) special
Source code in lutris/gui/dialogs/log.py
def __init__(self, title=None, buffer=None, application=None):
    super().__init__()
    ui_filename = os.path.join(datapath.get(), "ui/log-window.ui")
    builder = Gtk.Builder()
    builder.add_from_file(ui_filename)
    builder.connect_signals(self)
    window = builder.get_object("log_window")
    window.set_title(title)
    self.title = title

    self.buffer = buffer
    self.logtextview = LogTextView(self.buffer)

    scrolled_window = builder.get_object("scrolled_window")
    scrolled_window.add(self.logtextview)

    self.search_entry = builder.get_object("search_entry")
    self.search_entry.connect("search-changed", self.logtextview.find_first)
    self.search_entry.connect("next-match", self.logtextview.find_next)
    self.search_entry.connect("previous-match", self.logtextview.find_previous)

    save_button = builder.get_object("save_button")
    save_button.connect("clicked", self.on_save_clicked)

    window.connect("key-press-event", self.on_key_press_event)
    window.show_all()
on_key_press_event(self, widget, event)
Source code in lutris/gui/dialogs/log.py
def on_key_press_event(self, widget, event):
    shift = (event.state & Gdk.ModifierType.SHIFT_MASK)
    if event.keyval == Gdk.KEY_Return:
        if shift:
            self.search_entry.emit("previous-match")
        else:
            self.search_entry.emit("next-match")
on_save_clicked(self, _button)

Handler to save log to a file

Source code in lutris/gui/dialogs/log.py
def on_save_clicked(self, _button):
    """Handler to save log to a file"""
    now = datetime.now()
    log_filename = "%s (%s).log" % (self.title, now.strftime("%Y-%m-%d-%H-%M"))
    file_dialog = FileDialog(
        message="Save the logs to...",
        default_path=os.path.expanduser("~/%s" % log_filename),
        mode="save"
    )
    log_path = file_dialog.filename
    if not log_path:
        return

    text = self.buffer.get_text(
        self.buffer.get_start_iter(),
        self.buffer.get_end_iter(),
        True
    )
    with open(log_path, "w", encoding='utf-8') as log_file:
        log_file.write(text)

runner_install

Dialog used to install versions of a runner

RunnerInstallDialog (Dialog)

Dialog displaying available runner version and downloads them

Source code in lutris/gui/dialogs/runner_install.py
class RunnerInstallDialog(Dialog):
    """Dialog displaying available runner version and downloads them"""
    COL_VER = 0
    COL_ARCH = 1
    COL_URL = 2
    COL_INSTALLED = 3
    COL_PROGRESS = 4
    COL_USAGE = 5

    def __init__(self, title, parent, runner):
        super().__init__(title, parent, 0)
        self.add_buttons(_("_OK"), Gtk.ButtonsType.OK)
        self.runner = runner
        self.runner_info = {}
        self.installing = {}
        self.set_default_size(640, 480)
        self.runners = []
        self.listbox = None

        label = Gtk.Label.new(_("Waiting for response from %s") % settings.SITE_URL)
        self.vbox.pack_start(label, False, False, 18)

        spinner = Gtk.Spinner(visible=True)
        spinner.start()
        self.vbox.pack_start(spinner, False, False, 18)

        self.show_all()

        self.runner_store = Gtk.ListStore(str, str, str, bool, int, int)
        jobs.AsyncCall(api.get_runners, self.runner_fetch_cb, self.runner)

    def runner_fetch_cb(self, runner_info, error):
        """Clear the box and display versions from runner_info"""
        if error:
            logger.error(error)
            ErrorDialog(_("Unable to get runner versions: %s") % error)
            return

        self.runner_info = runner_info
        remote_versions = {(v["version"], v["architecture"]) for v in self.runner_info["versions"]}
        local_versions = self.get_installed_versions()
        for local_version in local_versions - remote_versions:
            self.runner_info["versions"].append({
                "version": local_version[0],
                "architecture": local_version[1],
                "url": "",
            })

        if not self.runner_info:
            ErrorDialog(_("Unable to get runner versions from lutris.net"))
            return

        for child_widget in self.vbox.get_children():
            if child_widget.get_name() not in "GtkBox":
                child_widget.destroy()

        label = Gtk.Label.new(_("%s version management") % self.runner_info["name"])
        self.vbox.add(label)
        self.installing = {}
        self.connect("response", self.on_destroy)

        scrolled_listbox = Gtk.ScrolledWindow()
        self.listbox = Gtk.ListBox()
        self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
        scrolled_listbox.add(self.listbox)
        self.vbox.pack_start(scrolled_listbox, True, True, 14)

        self.populate_store()
        self.show_all()
        self.populate_listboxrows(self.runner_store)

    def populate_listboxrows(self, store):
        for runner in store:
            row = Gtk.ListBoxRow()
            row.runner = runner
            hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
            row.hbox = hbox
            chk_installed = Gtk.CheckButton()
            chk_installed.set_sensitive(False)
            chk_installed.set_active(runner[self.COL_INSTALLED])
            hbox.pack_start(chk_installed, False, True, 0)
            row.chk_installed = chk_installed

            lbl_version = Gtk.Label(runner[self.COL_VER])
            lbl_version.set_max_width_chars(20)
            lbl_version.set_property("width-chars", 20)
            lbl_version.set_halign(Gtk.Align.START)
            hbox.pack_start(lbl_version, False, False, 5)

            arch_label = Gtk.Label(runner[self.COL_ARCH])
            arch_label.set_max_width_chars(8)
            arch_label.set_halign(Gtk.Align.START)
            hbox.pack_start(arch_label, False, True, 5)

            install_progress = Gtk.ProgressBar()
            install_progress.set_show_text(True)
            hbox.pack_end(install_progress, True, True, 5)
            row.install_progress = install_progress

            if runner[self.COL_INSTALLED]:
                # Check if there are apps installed, if so, show the view apps button
                app_count = runner[self.COL_USAGE] or 0
                if app_count > 0:
                    usage_button_text = gettext.ngettext(
                        "_View %d game",
                        "_View %d games",
                        app_count
                    ) % app_count

                    usage_button = Gtk.Button.new_with_mnemonic(usage_button_text)
                    usage_button.connect("button_press_event", self.on_show_apps_usage, row)
                    hbox.pack_end(usage_button, False, True, 2)

            button = Gtk.Button()
            hbox.pack_end(button, False, True, 0)
            hbox.reorder_child(button, 0)
            row.install_uninstall_cancel_button = button
            row.handler_id = None

            row.add(hbox)
            self.listbox.add(row)
            row.show_all()
            self.update_listboxrow(row)

    def update_listboxrow(self, row):
        row.install_progress.set_visible(False)
        row.chk_installed.set_active(row.runner[self.COL_INSTALLED])
        button = row.install_uninstall_cancel_button
        if row.handler_id is not None:
            button.disconnect(row.handler_id)
            row.handler_id = None
        if row.runner[self.COL_VER] in self.installing:
            button.set_label(_("Cancel"))
            handler_id = button.connect("button_press_event", self.on_cancel_install, row)
        else:
            if row.runner[self.COL_INSTALLED]:
                button.set_label(_("Uninstall"))
                handler_id = button.connect("button_press_event", self.on_uninstall_runner, row)
            else:
                button.set_label(_("Install"))
                handler_id = button.connect("button_press_event", self.on_install_runner, row)

        row.install_uninstall_cancel_button = button
        row.handler_id = handler_id

    def on_show_apps_usage(self, _widget, _button, row):
        """Return grid with games that uses this wine version"""
        runner = row.runner
        runner_version = "%s-%s" % (runner[self.COL_VER], runner[self.COL_ARCH])
        runner_games = get_games_by_runner(self.runner)
        apps = []
        for db_game in runner_games:
            if not db_game["installed"]:
                continue
            game = Game(db_game["id"])
            version = game.config.runner_config["version"]
            if version != runner_version:
                continue
            apps.append(game)

        dialog = ShowAppsDialog(_("Wine version usage"), self.get_toplevel(), runner_version, apps)
        dialog.run()

        dialog.destroy()

    def populate_store(self):
        """Return a ListStore populated with the runner versions"""
        version_usage = self.get_usage_stats()
        for version_info in reversed(self.runner_info["versions"]):
            is_installed = os.path.exists(self.get_runner_path(version_info["version"], version_info["architecture"]))
            games_using = version_usage.get("%(version)s-%(architecture)s" % version_info)
            self.runner_store.append(
                [
                    version_info["version"], version_info["architecture"], version_info["url"], is_installed, 0,
                    len(games_using) if games_using else 0
                ]
            )

    def get_installed_versions(self):
        """List versions available locally"""
        runner_path = os.path.join(settings.RUNNER_DIR, self.runner)
        if not os.path.exists(runner_path):
            return set()
        return {
            tuple(p.rsplit("-", 1))
            for p in os.listdir(runner_path)
            if "-" in p
        }

    def get_runner_path(self, version, arch):
        """Return the local path where the runner is/will be installed"""
        return os.path.join(settings.RUNNER_DIR, self.runner, "{}-{}".format(version, arch))

    def get_dest_path(self, row):
        """Return temporary path where the runners should be downloaded to"""
        return os.path.join(settings.CACHE_DIR, os.path.basename(row[self.COL_URL]))

    def on_installed_toggled(self, _widget, path):
        row = self.runner_store[path]
        if row[self.COL_VER] in self.installing:
            confirm_dlg = QuestionDialog(
                {
                    "question": _("Do you want to cancel the download?"),
                    "title": _("Download starting"),
                }
            )
            if confirm_dlg.result == confirm_dlg.YES:
                self.cancel_install(row)
        elif row[self.COL_INSTALLED]:
            self.uninstall_runner(row)
        else:
            self.install_runner(row)

    def on_cancel_install(self, widget, button, row):
        self.cancel_install(row)

    def cancel_install(self, row):
        """Cancel the installation of a runner version"""
        runner = row.runner
        self.installing[runner[self.COL_VER]].cancel()
        self.uninstall_runner(row)
        runner[self.COL_PROGRESS] = 0
        self.installing.pop(runner[self.COL_VER])
        self.update_listboxrow(row)
        row.install_progress.set_visible(False)

    def on_uninstall_runner(self, widget, button, row):
        self.uninstall_runner(row)

    def uninstall_runner(self, row):
        """Uninstall a runner version"""
        runner = row.runner
        version = runner[self.COL_VER]
        arch = runner[self.COL_ARCH]
        system.remove_folder(self.get_runner_path(version, arch))
        runner[self.COL_INSTALLED] = False
        if self.runner == "wine":
            logger.debug("Clearing wine version cache")
            from lutris.util.wine.wine import get_wine_versions

            get_wine_versions.cache_clear()
        self.update_listboxrow(row)

    def on_install_runner(self, _widget, _button, row):
        self.install_runner(row)

    def install_runner(self, row):
        """Download and install a runner version"""
        runner = row.runner
        row.install_progress.set_fraction(0.0)
        dest_path = self.get_dest_path(runner)
        url = runner[self.COL_URL]
        if not url:
            ErrorDialog(_("Version %s is not longer available") % runner[self.COL_VER])
            return
        downloader = Downloader(runner[self.COL_URL], dest_path, overwrite=True)
        GLib.timeout_add(100, self.get_progress, downloader, row)
        self.installing[runner[self.COL_VER]] = downloader
        downloader.start()
        self.update_listboxrow(row)

    def get_progress(self, downloader, row):
        """Update progress bar with download progress"""
        runner = row.runner
        if downloader.state == downloader.CANCELLED:
            return False
        if downloader.state == downloader.ERROR:
            self.cancel_install(row)
            return False
        row.install_progress.show()
        downloader.check_progress()
        percent_downloaded = downloader.progress_percentage
        if percent_downloaded >= 1:
            runner[self.COL_PROGRESS] = percent_downloaded
            row.install_progress.set_fraction(percent_downloaded / 100)
        else:
            runner[self.COL_PROGRESS] = 1
            row.install_progress.pulse()
            row.install_progress.set_text = _("Downloading…")
        if downloader.state == downloader.COMPLETED:
            runner[self.COL_PROGRESS] = 99
            row.install_progress.set_text = _("Extracting…")
            self.on_runner_downloaded(row)
            return False
        return True

    def progress_pulse(self, row):
        runner = row.runner
        row.install_progress.pulse()
        return not runner[self.COL_INSTALLED]

    def get_usage_stats(self):
        """Return the usage for each version"""
        runner_games = get_games_by_runner(self.runner)
        version_usage = defaultdict(list)
        for db_game in runner_games:
            if not db_game["installed"]:
                continue
            game = Game(db_game["id"])
            version = game.config.runner_config["version"]
            version_usage[version].append(db_game["id"])
        return version_usage

    def on_runner_downloaded(self, row):
        """Handler called when a runner version is downloaded"""
        runner = row.runner
        version = runner[self.COL_VER]
        architecture = runner[self.COL_ARCH]
        logger.debug("Runner %s for %s has finished downloading", version, architecture)
        src = self.get_dest_path(runner)
        dst = self.get_runner_path(version, architecture)
        GLib.timeout_add(100, self.progress_pulse, row)
        jobs.AsyncCall(self.extract, self.on_extracted, src, dst, row)

    @staticmethod
    def extract(src, dst, row):
        """Extract a runner archive to a destination"""
        extract_archive(src, dst)
        return src, row

    def on_extracted(self, row_info, error):
        """Called when a runner archive is extracted"""
        if error or not row_info:
            ErrorDialog(_("Failed to retrieve the runner archive"), parent=self)
            return
        src, row = row_info
        runner = row.runner
        os.remove(src)
        runner[self.COL_PROGRESS] = 0
        runner[self.COL_INSTALLED] = True
        self.installing.pop(runner[self.COL_VER])
        row.install_progress.set_text = ""
        row.install_progress.set_fraction(0.0)
        row.install_progress.hide()
        self.update_listboxrow(row)
        if self.runner == "wine":
            logger.debug("Clearing wine version cache")
            from lutris.util.wine.wine import get_wine_versions
            get_wine_versions.cache_clear()

    def on_destroy(self, _dialog, _data=None):
        """Override delete handler to prevent closing while downloads are active"""
        if self.installing:
            return True
        self.destroy()
        return True
COL_ARCH
COL_INSTALLED
COL_PROGRESS
COL_URL
COL_USAGE
COL_VER
__init__(self, title, parent, runner) special
Source code in lutris/gui/dialogs/runner_install.py
def __init__(self, title, parent, runner):
    super().__init__(title, parent, 0)
    self.add_buttons(_("_OK"), Gtk.ButtonsType.OK)
    self.runner = runner
    self.runner_info = {}
    self.installing = {}
    self.set_default_size(640, 480)
    self.runners = []
    self.listbox = None

    label = Gtk.Label.new(_("Waiting for response from %s") % settings.SITE_URL)
    self.vbox.pack_start(label, False, False, 18)

    spinner = Gtk.Spinner(visible=True)
    spinner.start()
    self.vbox.pack_start(spinner, False, False, 18)

    self.show_all()

    self.runner_store = Gtk.ListStore(str, str, str, bool, int, int)
    jobs.AsyncCall(api.get_runners, self.runner_fetch_cb, self.runner)
cancel_install(self, row)

Cancel the installation of a runner version

Source code in lutris/gui/dialogs/runner_install.py
def cancel_install(self, row):
    """Cancel the installation of a runner version"""
    runner = row.runner
    self.installing[runner[self.COL_VER]].cancel()
    self.uninstall_runner(row)
    runner[self.COL_PROGRESS] = 0
    self.installing.pop(runner[self.COL_VER])
    self.update_listboxrow(row)
    row.install_progress.set_visible(False)
extract(src, dst, row) staticmethod

Extract a runner archive to a destination

Source code in lutris/gui/dialogs/runner_install.py
@staticmethod
def extract(src, dst, row):
    """Extract a runner archive to a destination"""
    extract_archive(src, dst)
    return src, row
get_dest_path(self, row)

Return temporary path where the runners should be downloaded to

Source code in lutris/gui/dialogs/runner_install.py
def get_dest_path(self, row):
    """Return temporary path where the runners should be downloaded to"""
    return os.path.join(settings.CACHE_DIR, os.path.basename(row[self.COL_URL]))
get_installed_versions(self)

List versions available locally

Source code in lutris/gui/dialogs/runner_install.py
def get_installed_versions(self):
    """List versions available locally"""
    runner_path = os.path.join(settings.RUNNER_DIR, self.runner)
    if not os.path.exists(runner_path):
        return set()
    return {
        tuple(p.rsplit("-", 1))
        for p in os.listdir(runner_path)
        if "-" in p
    }
get_progress(self, downloader, row)

Update progress bar with download progress

Source code in lutris/gui/dialogs/runner_install.py
def get_progress(self, downloader, row):
    """Update progress bar with download progress"""
    runner = row.runner
    if downloader.state == downloader.CANCELLED:
        return False
    if downloader.state == downloader.ERROR:
        self.cancel_install(row)
        return False
    row.install_progress.show()
    downloader.check_progress()
    percent_downloaded = downloader.progress_percentage
    if percent_downloaded >= 1:
        runner[self.COL_PROGRESS] = percent_downloaded
        row.install_progress.set_fraction(percent_downloaded / 100)
    else:
        runner[self.COL_PROGRESS] = 1
        row.install_progress.pulse()
        row.install_progress.set_text = _("Downloading…")
    if downloader.state == downloader.COMPLETED:
        runner[self.COL_PROGRESS] = 99
        row.install_progress.set_text = _("Extracting…")
        self.on_runner_downloaded(row)
        return False
    return True
get_runner_path(self, version, arch)

Return the local path where the runner is/will be installed

Source code in lutris/gui/dialogs/runner_install.py
def get_runner_path(self, version, arch):
    """Return the local path where the runner is/will be installed"""
    return os.path.join(settings.RUNNER_DIR, self.runner, "{}-{}".format(version, arch))
get_usage_stats(self)

Return the usage for each version

Source code in lutris/gui/dialogs/runner_install.py
def get_usage_stats(self):
    """Return the usage for each version"""
    runner_games = get_games_by_runner(self.runner)
    version_usage = defaultdict(list)
    for db_game in runner_games:
        if not db_game["installed"]:
            continue
        game = Game(db_game["id"])
        version = game.config.runner_config["version"]
        version_usage[version].append(db_game["id"])
    return version_usage
install_runner(self, row)

Download and install a runner version

Source code in lutris/gui/dialogs/runner_install.py
def install_runner(self, row):
    """Download and install a runner version"""
    runner = row.runner
    row.install_progress.set_fraction(0.0)
    dest_path = self.get_dest_path(runner)
    url = runner[self.COL_URL]
    if not url:
        ErrorDialog(_("Version %s is not longer available") % runner[self.COL_VER])
        return
    downloader = Downloader(runner[self.COL_URL], dest_path, overwrite=True)
    GLib.timeout_add(100, self.get_progress, downloader, row)
    self.installing[runner[self.COL_VER]] = downloader
    downloader.start()
    self.update_listboxrow(row)
on_cancel_install(self, widget, button, row)
Source code in lutris/gui/dialogs/runner_install.py
def on_cancel_install(self, widget, button, row):
    self.cancel_install(row)
on_destroy(self, _dialog, _data=None)

Override delete handler to prevent closing while downloads are active

Source code in lutris/gui/dialogs/runner_install.py
def on_destroy(self, _dialog, _data=None):
    """Override delete handler to prevent closing while downloads are active"""
    if self.installing:
        return True
    self.destroy()
    return True
on_extracted(self, row_info, error)

Called when a runner archive is extracted

Source code in lutris/gui/dialogs/runner_install.py
def on_extracted(self, row_info, error):
    """Called when a runner archive is extracted"""
    if error or not row_info:
        ErrorDialog(_("Failed to retrieve the runner archive"), parent=self)
        return
    src, row = row_info
    runner = row.runner
    os.remove(src)
    runner[self.COL_PROGRESS] = 0
    runner[self.COL_INSTALLED] = True
    self.installing.pop(runner[self.COL_VER])
    row.install_progress.set_text = ""
    row.install_progress.set_fraction(0.0)
    row.install_progress.hide()
    self.update_listboxrow(row)
    if self.runner == "wine":
        logger.debug("Clearing wine version cache")
        from lutris.util.wine.wine import get_wine_versions
        get_wine_versions.cache_clear()
on_install_runner(self, _widget, _button, row)
Source code in lutris/gui/dialogs/runner_install.py
def on_install_runner(self, _widget, _button, row):
    self.install_runner(row)
on_installed_toggled(self, _widget, path)
Source code in lutris/gui/dialogs/runner_install.py
def on_installed_toggled(self, _widget, path):
    row = self.runner_store[path]
    if row[self.COL_VER] in self.installing:
        confirm_dlg = QuestionDialog(
            {
                "question": _("Do you want to cancel the download?"),
                "title": _("Download starting"),
            }
        )
        if confirm_dlg.result == confirm_dlg.YES:
            self.cancel_install(row)
    elif row[self.COL_INSTALLED]:
        self.uninstall_runner(row)
    else:
        self.install_runner(row)
on_runner_downloaded(self, row)

Handler called when a runner version is downloaded

Source code in lutris/gui/dialogs/runner_install.py
def on_runner_downloaded(self, row):
    """Handler called when a runner version is downloaded"""
    runner = row.runner
    version = runner[self.COL_VER]
    architecture = runner[self.COL_ARCH]
    logger.debug("Runner %s for %s has finished downloading", version, architecture)
    src = self.get_dest_path(runner)
    dst = self.get_runner_path(version, architecture)
    GLib.timeout_add(100, self.progress_pulse, row)
    jobs.AsyncCall(self.extract, self.on_extracted, src, dst, row)
on_show_apps_usage(self, _widget, _button, row)

Return grid with games that uses this wine version

Source code in lutris/gui/dialogs/runner_install.py
def on_show_apps_usage(self, _widget, _button, row):
    """Return grid with games that uses this wine version"""
    runner = row.runner
    runner_version = "%s-%s" % (runner[self.COL_VER], runner[self.COL_ARCH])
    runner_games = get_games_by_runner(self.runner)
    apps = []
    for db_game in runner_games:
        if not db_game["installed"]:
            continue
        game = Game(db_game["id"])
        version = game.config.runner_config["version"]
        if version != runner_version:
            continue
        apps.append(game)

    dialog = ShowAppsDialog(_("Wine version usage"), self.get_toplevel(), runner_version, apps)
    dialog.run()

    dialog.destroy()
on_uninstall_runner(self, widget, button, row)
Source code in lutris/gui/dialogs/runner_install.py
def on_uninstall_runner(self, widget, button, row):
    self.uninstall_runner(row)
populate_listboxrows(self, store)
Source code in lutris/gui/dialogs/runner_install.py
def populate_listboxrows(self, store):
    for runner in store:
        row = Gtk.ListBoxRow()
        row.runner = runner
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        row.hbox = hbox
        chk_installed = Gtk.CheckButton()
        chk_installed.set_sensitive(False)
        chk_installed.set_active(runner[self.COL_INSTALLED])
        hbox.pack_start(chk_installed, False, True, 0)
        row.chk_installed = chk_installed

        lbl_version = Gtk.Label(runner[self.COL_VER])
        lbl_version.set_max_width_chars(20)
        lbl_version.set_property("width-chars", 20)
        lbl_version.set_halign(Gtk.Align.START)
        hbox.pack_start(lbl_version, False, False, 5)

        arch_label = Gtk.Label(runner[self.COL_ARCH])
        arch_label.set_max_width_chars(8)
        arch_label.set_halign(Gtk.Align.START)
        hbox.pack_start(arch_label, False, True, 5)

        install_progress = Gtk.ProgressBar()
        install_progress.set_show_text(True)
        hbox.pack_end(install_progress, True, True, 5)
        row.install_progress = install_progress

        if runner[self.COL_INSTALLED]:
            # Check if there are apps installed, if so, show the view apps button
            app_count = runner[self.COL_USAGE] or 0
            if app_count > 0:
                usage_button_text = gettext.ngettext(
                    "_View %d game",
                    "_View %d games",
                    app_count
                ) % app_count

                usage_button = Gtk.Button.new_with_mnemonic(usage_button_text)
                usage_button.connect("button_press_event", self.on_show_apps_usage, row)
                hbox.pack_end(usage_button, False, True, 2)

        button = Gtk.Button()
        hbox.pack_end(button, False, True, 0)
        hbox.reorder_child(button, 0)
        row.install_uninstall_cancel_button = button
        row.handler_id = None

        row.add(hbox)
        self.listbox.add(row)
        row.show_all()
        self.update_listboxrow(row)
populate_store(self)

Return a ListStore populated with the runner versions

Source code in lutris/gui/dialogs/runner_install.py
def populate_store(self):
    """Return a ListStore populated with the runner versions"""
    version_usage = self.get_usage_stats()
    for version_info in reversed(self.runner_info["versions"]):
        is_installed = os.path.exists(self.get_runner_path(version_info["version"], version_info["architecture"]))
        games_using = version_usage.get("%(version)s-%(architecture)s" % version_info)
        self.runner_store.append(
            [
                version_info["version"], version_info["architecture"], version_info["url"], is_installed, 0,
                len(games_using) if games_using else 0
            ]
        )
progress_pulse(self, row)
Source code in lutris/gui/dialogs/runner_install.py
def progress_pulse(self, row):
    runner = row.runner
    row.install_progress.pulse()
    return not runner[self.COL_INSTALLED]
runner_fetch_cb(self, runner_info, error)

Clear the box and display versions from runner_info

Source code in lutris/gui/dialogs/runner_install.py
def runner_fetch_cb(self, runner_info, error):
    """Clear the box and display versions from runner_info"""
    if error:
        logger.error(error)
        ErrorDialog(_("Unable to get runner versions: %s") % error)
        return

    self.runner_info = runner_info
    remote_versions = {(v["version"], v["architecture"]) for v in self.runner_info["versions"]}
    local_versions = self.get_installed_versions()
    for local_version in local_versions - remote_versions:
        self.runner_info["versions"].append({
            "version": local_version[0],
            "architecture": local_version[1],
            "url": "",
        })

    if not self.runner_info:
        ErrorDialog(_("Unable to get runner versions from lutris.net"))
        return

    for child_widget in self.vbox.get_children():
        if child_widget.get_name() not in "GtkBox":
            child_widget.destroy()

    label = Gtk.Label.new(_("%s version management") % self.runner_info["name"])
    self.vbox.add(label)
    self.installing = {}
    self.connect("response", self.on_destroy)

    scrolled_listbox = Gtk.ScrolledWindow()
    self.listbox = Gtk.ListBox()
    self.listbox.set_selection_mode(Gtk.SelectionMode.NONE)
    scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
    scrolled_listbox.add(self.listbox)
    self.vbox.pack_start(scrolled_listbox, True, True, 14)

    self.populate_store()
    self.show_all()
    self.populate_listboxrows(self.runner_store)
uninstall_runner(self, row)

Uninstall a runner version

Source code in lutris/gui/dialogs/runner_install.py
def uninstall_runner(self, row):
    """Uninstall a runner version"""
    runner = row.runner
    version = runner[self.COL_VER]
    arch = runner[self.COL_ARCH]
    system.remove_folder(self.get_runner_path(version, arch))
    runner[self.COL_INSTALLED] = False
    if self.runner == "wine":
        logger.debug("Clearing wine version cache")
        from lutris.util.wine.wine import get_wine_versions

        get_wine_versions.cache_clear()
    self.update_listboxrow(row)
update_listboxrow(self, row)
Source code in lutris/gui/dialogs/runner_install.py
def update_listboxrow(self, row):
    row.install_progress.set_visible(False)
    row.chk_installed.set_active(row.runner[self.COL_INSTALLED])
    button = row.install_uninstall_cancel_button
    if row.handler_id is not None:
        button.disconnect(row.handler_id)
        row.handler_id = None
    if row.runner[self.COL_VER] in self.installing:
        button.set_label(_("Cancel"))
        handler_id = button.connect("button_press_event", self.on_cancel_install, row)
    else:
        if row.runner[self.COL_INSTALLED]:
            button.set_label(_("Uninstall"))
            handler_id = button.connect("button_press_event", self.on_uninstall_runner, row)
        else:
            button.set_label(_("Install"))
            handler_id = button.connect("button_press_event", self.on_install_runner, row)

    row.install_uninstall_cancel_button = button
    row.handler_id = handler_id
ShowAppsDialog (Dialog)
Source code in lutris/gui/dialogs/runner_install.py
class ShowAppsDialog(Dialog):
    def __init__(self, title, parent, runner_version, apps):
        super().__init__(title, parent, Gtk.DialogFlags.MODAL)
        self.add_buttons(
            Gtk.STOCK_OK, Gtk.ResponseType.OK
        )

        self.set_default_size(400, 500)

        label = Gtk.Label.new(_("Showing games using %s") % runner_version)
        self.vbox.add(label)
        scrolled_listbox = Gtk.ScrolledWindow()
        listbox = Gtk.ListBox()
        listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
        scrolled_listbox.add(listbox)
        self.vbox.pack_start(scrolled_listbox, True, True, 14)

        for app in apps:
            row = Gtk.ListBoxRow()
            hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

            lbl_game = Gtk.Label(app.name)
            lbl_game.set_halign(Gtk.Align.START)
            hbox.pack_start(lbl_game, True, True, 5)
            row.add(hbox)
            listbox.add(row)

        self.show_all()
__init__(self, title, parent, runner_version, apps) special
Source code in lutris/gui/dialogs/runner_install.py
def __init__(self, title, parent, runner_version, apps):
    super().__init__(title, parent, Gtk.DialogFlags.MODAL)
    self.add_buttons(
        Gtk.STOCK_OK, Gtk.ResponseType.OK
    )

    self.set_default_size(400, 500)

    label = Gtk.Label.new(_("Showing games using %s") % runner_version)
    self.vbox.add(label)
    scrolled_listbox = Gtk.ScrolledWindow()
    listbox = Gtk.ListBox()
    listbox.set_selection_mode(Gtk.SelectionMode.NONE)
    scrolled_listbox.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
    scrolled_listbox.set_shadow_type(Gtk.ShadowType.ETCHED_OUT)
    scrolled_listbox.add(listbox)
    self.vbox.pack_start(scrolled_listbox, True, True, 14)

    for app in apps:
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)

        lbl_game = Gtk.Label(app.name)
        lbl_game.set_halign(Gtk.Align.START)
        hbox.pack_start(lbl_game, True, True, 5)
        row.add(hbox)
        listbox.add(row)

    self.show_all()

uninstall_game

RemoveGameDialog (Dialog)
Source code in lutris/gui/dialogs/uninstall_game.py
class RemoveGameDialog(Dialog):
    def __init__(self, game_id, parent=None):
        super().__init__(parent=parent)
        self.set_size_request(640, 128)
        self.game = Game(game_id)
        container = Gtk.VBox(visible=True)
        self.get_content_area().add(container)

        title_label = Gtk.Label(visible=True)
        title_label.set_line_wrap(True)
        title_label.set_alignment(0, 0.5)
        title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        title_label.set_markup(_("<span font_desc='14'><b>Remove %s</b></span>") % gtk_safe(self.game.name))
        container.pack_start(title_label, False, False, 4)

        self.delete_label = Gtk.Label(visible=True)
        self.delete_label.set_alignment(0, 0.5)
        self.delete_label.set_markup(
            _("Completely remove %s from the library?\nAll play time will be lost.") % self.game)
        container.pack_start(self.delete_label, False, False, 4)

        button_box = Gtk.HBox(visible=True)
        button_box.set_margin_top(30)
        style_context = button_box.get_style_context()
        style_context.add_class("linked")
        cancel_button = Gtk.Button(_("Cancel"), visible=True)
        cancel_button.connect("clicked", self.on_close)
        button_box.add(cancel_button)

        self.remove_button = Gtk.Button(_("Remove"), visible=True)
        self.remove_button.connect("clicked", self.on_remove_clicked)

        button_box.add(self.remove_button)
        container.pack_end(button_box, False, False, 0)
        self.show()

    def on_close(self, _button):
        self.destroy()

    def on_remove_clicked(self, button):
        button.set_sensitive(False)
        self.game.delete()
        self.destroy()
__init__(self, game_id, parent=None) special
Source code in lutris/gui/dialogs/uninstall_game.py
def __init__(self, game_id, parent=None):
    super().__init__(parent=parent)
    self.set_size_request(640, 128)
    self.game = Game(game_id)
    container = Gtk.VBox(visible=True)
    self.get_content_area().add(container)

    title_label = Gtk.Label(visible=True)
    title_label.set_line_wrap(True)
    title_label.set_alignment(0, 0.5)
    title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
    title_label.set_markup(_("<span font_desc='14'><b>Remove %s</b></span>") % gtk_safe(self.game.name))
    container.pack_start(title_label, False, False, 4)

    self.delete_label = Gtk.Label(visible=True)
    self.delete_label.set_alignment(0, 0.5)
    self.delete_label.set_markup(
        _("Completely remove %s from the library?\nAll play time will be lost.") % self.game)
    container.pack_start(self.delete_label, False, False, 4)

    button_box = Gtk.HBox(visible=True)
    button_box.set_margin_top(30)
    style_context = button_box.get_style_context()
    style_context.add_class("linked")
    cancel_button = Gtk.Button(_("Cancel"), visible=True)
    cancel_button.connect("clicked", self.on_close)
    button_box.add(cancel_button)

    self.remove_button = Gtk.Button(_("Remove"), visible=True)
    self.remove_button.connect("clicked", self.on_remove_clicked)

    button_box.add(self.remove_button)
    container.pack_end(button_box, False, False, 0)
    self.show()
on_close(self, _button)
Source code in lutris/gui/dialogs/uninstall_game.py
def on_close(self, _button):
    self.destroy()
on_remove_clicked(self, button)
Source code in lutris/gui/dialogs/uninstall_game.py
def on_remove_clicked(self, button):
    button.set_sensitive(False)
    self.game.delete()
    self.destroy()
UninstallGameDialog (Dialog)
Source code in lutris/gui/dialogs/uninstall_game.py
class UninstallGameDialog(Dialog):
    def __init__(self, game_id, parent=None):
        super().__init__(parent=parent)
        self.set_size_request(640, 128)
        self.game = Game(game_id)
        self.delete_files = False
        container = Gtk.VBox(visible=True)
        self.get_content_area().add(container)

        title_label = Gtk.Label(visible=True)
        title_label.set_line_wrap(True)
        title_label.set_alignment(0, 0.5)
        title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        title_label.set_markup(_("<span font_desc='14'><b>Uninstall %s</b></span>") % gtk_safe(self.game.name))

        container.pack_start(title_label, False, False, 4)

        self.folder_label = Gtk.Label(visible=True)
        self.folder_label.set_alignment(0, 0.5)

        self.delete_button = Gtk.Button(_("Uninstall"), visible=True)
        self.delete_button.connect("clicked", self.on_delete_clicked)

        if not self.game.directory:
            self.folder_label.set_markup(_("No file will be deleted"))
        elif len(get_games(filters={"directory": self.game.directory})) > 1:
            self.folder_label.set_markup(
                _("The folder %s is used by other games and will be kept.") % self.game.directory)
        elif is_removeable(self.game.directory):
            self.delete_button.set_sensitive(False)
            self.folder_label.set_markup(_("<i>Calculating size…</i>"))
            AsyncCall(get_disk_size, self.folder_size_cb, self.game.directory)
        else:
            self.folder_label.set_markup(
                _("Content of %s are protected and will not be deleted.") % reverse_expanduser(self.game.directory)
            )
        container.pack_start(self.folder_label, False, False, 4)

        self.confirm_delete_button = Gtk.CheckButton()
        self.confirm_delete_button.set_active(True)
        container.pack_start(self.confirm_delete_button, False, False, 4)

        button_box = Gtk.HBox(visible=True)
        button_box.set_margin_top(30)
        style_context = button_box.get_style_context()
        style_context.add_class("linked")
        cancel_button = Gtk.Button(_("Cancel"), visible=True)
        cancel_button.connect("clicked", self.on_close)
        button_box.add(cancel_button)
        button_box.add(self.delete_button)
        container.pack_end(button_box, False, False, 0)
        self.show()

    def folder_size_cb(self, folder_size, error):
        if error:
            logger.error(error)
            return
        self.delete_files = True
        self.delete_button.set_sensitive(True)
        self.folder_label.hide()
        self.confirm_delete_button.show()
        self.confirm_delete_button.set_label(
            _("Delete %s (%s)") % (
                reverse_expanduser(self.game.directory),
                human_size(folder_size)
            )
        )

    def on_close(self, _button):
        self.destroy()

    def on_delete_clicked(self, button):
        button.set_sensitive(False)
        if not self.confirm_delete_button.get_active():
            self.delete_files = False
        if self.delete_files and not hasattr(self.game.runner, "no_game_remove_warning"):
            dlg = QuestionDialog(
                {
                    "question": _(
                        "Please confirm.\nEverything under <b>%s</b>\n"
                        "will be deleted."
                    ) % gtk_safe(self.game.directory),
                    "title": _("Permanently delete files?"),
                }
            )
            if dlg.result != Gtk.ResponseType.YES:
                button.set_sensitive(True)
                return
        if self.delete_files:
            self.folder_label.set_markup(_("Uninstalling game and deleting files..."))
        else:
            self.folder_label.set_markup(_("Uninstalling game..."))
        self.game.remove(self.delete_files)
        self.destroy()
__init__(self, game_id, parent=None) special
Source code in lutris/gui/dialogs/uninstall_game.py
def __init__(self, game_id, parent=None):
    super().__init__(parent=parent)
    self.set_size_request(640, 128)
    self.game = Game(game_id)
    self.delete_files = False
    container = Gtk.VBox(visible=True)
    self.get_content_area().add(container)

    title_label = Gtk.Label(visible=True)
    title_label.set_line_wrap(True)
    title_label.set_alignment(0, 0.5)
    title_label.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
    title_label.set_markup(_("<span font_desc='14'><b>Uninstall %s</b></span>") % gtk_safe(self.game.name))

    container.pack_start(title_label, False, False, 4)

    self.folder_label = Gtk.Label(visible=True)
    self.folder_label.set_alignment(0, 0.5)

    self.delete_button = Gtk.Button(_("Uninstall"), visible=True)
    self.delete_button.connect("clicked", self.on_delete_clicked)

    if not self.game.directory:
        self.folder_label.set_markup(_("No file will be deleted"))
    elif len(get_games(filters={"directory": self.game.directory})) > 1:
        self.folder_label.set_markup(
            _("The folder %s is used by other games and will be kept.") % self.game.directory)
    elif is_removeable(self.game.directory):
        self.delete_button.set_sensitive(False)
        self.folder_label.set_markup(_("<i>Calculating size…</i>"))
        AsyncCall(get_disk_size, self.folder_size_cb, self.game.directory)
    else:
        self.folder_label.set_markup(
            _("Content of %s are protected and will not be deleted.") % reverse_expanduser(self.game.directory)
        )
    container.pack_start(self.folder_label, False, False, 4)

    self.confirm_delete_button = Gtk.CheckButton()
    self.confirm_delete_button.set_active(True)
    container.pack_start(self.confirm_delete_button, False, False, 4)

    button_box = Gtk.HBox(visible=True)
    button_box.set_margin_top(30)
    style_context = button_box.get_style_context()
    style_context.add_class("linked")
    cancel_button = Gtk.Button(_("Cancel"), visible=True)
    cancel_button.connect("clicked", self.on_close)
    button_box.add(cancel_button)
    button_box.add(self.delete_button)
    container.pack_end(button_box, False, False, 0)
    self.show()
folder_size_cb(self, folder_size, error)
Source code in lutris/gui/dialogs/uninstall_game.py
def folder_size_cb(self, folder_size, error):
    if error:
        logger.error(error)
        return
    self.delete_files = True
    self.delete_button.set_sensitive(True)
    self.folder_label.hide()
    self.confirm_delete_button.show()
    self.confirm_delete_button.set_label(
        _("Delete %s (%s)") % (
            reverse_expanduser(self.game.directory),
            human_size(folder_size)
        )
    )
on_close(self, _button)
Source code in lutris/gui/dialogs/uninstall_game.py
def on_close(self, _button):
    self.destroy()
on_delete_clicked(self, button)
Source code in lutris/gui/dialogs/uninstall_game.py
def on_delete_clicked(self, button):
    button.set_sensitive(False)
    if not self.confirm_delete_button.get_active():
        self.delete_files = False
    if self.delete_files and not hasattr(self.game.runner, "no_game_remove_warning"):
        dlg = QuestionDialog(
            {
                "question": _(
                    "Please confirm.\nEverything under <b>%s</b>\n"
                    "will be deleted."
                ) % gtk_safe(self.game.directory),
                "title": _("Permanently delete files?"),
            }
        )
        if dlg.result != Gtk.ResponseType.YES:
            button.set_sensitive(True)
            return
    if self.delete_files:
        self.folder_label.set_markup(_("Uninstalling game and deleting files..."))
    else:
        self.folder_label.set_markup(_("Uninstalling game..."))
    self.game.remove(self.delete_files)
    self.destroy()

webconnect_dialog

isort:skip_file

WebConnectDialog (Dialog)

Login form for external services

Source code in lutris/gui/dialogs/webconnect_dialog.py
class WebConnectDialog(Dialog):
    """Login form for external services"""

    def __init__(self, service, parent=None):

        self.context = WebKit2.WebContext.new()
        if "http_proxy" in os.environ:
            proxy = WebKit2.NetworkProxySettings.new(os.environ["http_proxy"])
            self.context.set_network_proxy_settings(WebKit2.NetworkProxyMode.CUSTOM, proxy)
        WebKit2.CookieManager.set_persistent_storage(
            self.context.get_cookie_manager(),
            service.cookies_path,
            WebKit2.CookiePersistentStorage(0),
        )
        self.service = service

        super().__init__(title=service.name, parent=parent)
        self.set_border_width(0)
        self.set_default_size(390, 500)

        self.webview = WebKit2.WebView.new_with_context(self.context)
        self.webview.load_uri(service.login_url)
        self.webview.connect("load-changed", self.on_navigation)
        self.webview.connect("create", self.on_webview_popup)
        self.vbox.pack_start(self.webview, True, True, 0)  # pylint: disable=no-member

        webkit_settings = self.webview.get_settings()
        # Allow popups (Doesn't work...)
        webkit_settings.set_enable_write_console_messages_to_stdout(True)
        webkit_settings.set_allow_modal_dialogs(True)
        # Enable developer options for troubleshooting (Can be disabled in
        # releases)
        webkit_settings.set_javascript_can_open_windows_automatically(True)
        webkit_settings.set_enable_developer_extras(True)

        self.show_all()

    def enable_inspector(self):
        """If you want a full blown Webkit inspector, call this"""
        inspector = self.webview.get_inspector()
        inspector.show()

    def on_navigation(self, widget, load_event):
        if load_event == WebKit2.LoadEvent.FINISHED:
            url = widget.get_uri()
            if url in self.service.scripts:
                script = self.service.scripts[url]
                widget.run_javascript(script, None, None)
                return True
            if url.startswith(self.service.redirect_uri):
                if self.service.requires_login_page:
                    resource = widget.get_main_resource()
                    resource.get_data(None, self._get_response_data_finish, None)
                else:
                    self.service.login_callback(url)
                    self.destroy()
        return True

    def _get_response_data_finish(self, resource, result, user_data=None):
        html_response = resource.get_data_finish(result)
        self.service.login_callback(html_response)
        self.destroy()

    def on_webview_popup(self, widget, navigation_action):
        """Handles web popups created by this dialog's webview"""
        uri = navigation_action.get_request().get_uri()
        view = WebKit2.WebView.new_with_related_view(widget)
        view.load_uri(uri)
        popup_dialog = WebPopupDialog(view, parent=self)
        popup_dialog.set_modal(True)
        popup_dialog.show()
        return view
__init__(self, service, parent=None) special
Source code in lutris/gui/dialogs/webconnect_dialog.py
def __init__(self, service, parent=None):

    self.context = WebKit2.WebContext.new()
    if "http_proxy" in os.environ:
        proxy = WebKit2.NetworkProxySettings.new(os.environ["http_proxy"])
        self.context.set_network_proxy_settings(WebKit2.NetworkProxyMode.CUSTOM, proxy)
    WebKit2.CookieManager.set_persistent_storage(
        self.context.get_cookie_manager(),
        service.cookies_path,
        WebKit2.CookiePersistentStorage(0),
    )
    self.service = service

    super().__init__(title=service.name, parent=parent)
    self.set_border_width(0)
    self.set_default_size(390, 500)

    self.webview = WebKit2.WebView.new_with_context(self.context)
    self.webview.load_uri(service.login_url)
    self.webview.connect("load-changed", self.on_navigation)
    self.webview.connect("create", self.on_webview_popup)
    self.vbox.pack_start(self.webview, True, True, 0)  # pylint: disable=no-member

    webkit_settings = self.webview.get_settings()
    # Allow popups (Doesn't work...)
    webkit_settings.set_enable_write_console_messages_to_stdout(True)
    webkit_settings.set_allow_modal_dialogs(True)
    # Enable developer options for troubleshooting (Can be disabled in
    # releases)
    webkit_settings.set_javascript_can_open_windows_automatically(True)
    webkit_settings.set_enable_developer_extras(True)

    self.show_all()
enable_inspector(self)

If you want a full blown Webkit inspector, call this

Source code in lutris/gui/dialogs/webconnect_dialog.py
def enable_inspector(self):
    """If you want a full blown Webkit inspector, call this"""
    inspector = self.webview.get_inspector()
    inspector.show()
on_navigation(self, widget, load_event)
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_navigation(self, widget, load_event):
    if load_event == WebKit2.LoadEvent.FINISHED:
        url = widget.get_uri()
        if url in self.service.scripts:
            script = self.service.scripts[url]
            widget.run_javascript(script, None, None)
            return True
        if url.startswith(self.service.redirect_uri):
            if self.service.requires_login_page:
                resource = widget.get_main_resource()
                resource.get_data(None, self._get_response_data_finish, None)
            else:
                self.service.login_callback(url)
                self.destroy()
    return True
on_webview_popup(self, widget, navigation_action)

Handles web popups created by this dialog's webview

Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_webview_popup(self, widget, navigation_action):
    """Handles web popups created by this dialog's webview"""
    uri = navigation_action.get_request().get_uri()
    view = WebKit2.WebView.new_with_related_view(widget)
    view.load_uri(uri)
    popup_dialog = WebPopupDialog(view, parent=self)
    popup_dialog.set_modal(True)
    popup_dialog.show()
    return view
WebPopupDialog (Dialog)

Dialog for handling web popups

Source code in lutris/gui/dialogs/webconnect_dialog.py
class WebPopupDialog(Dialog):
    """Dialog for handling web popups"""

    def __init__(self, webview, parent=None):
        # pylint: disable=no-member
        self.parent = parent
        super().__init__(title=_('Loading...'), parent=parent)
        self.webview = webview
        self.webview.connect("ready-to-show", self.on_ready_webview)
        self.webview.connect("notify::title", self.on_available_webview_title)
        self.webview.connect("create", self.on_new_webview_popup)
        self.webview.connect("close", self.on_webview_close)
        self.vbox.pack_start(self.webview, True, True, 0)
        self.set_border_width(0)
        self.set_default_size(390, 500)

    def on_ready_webview(self, webview):
        self.show_all()

    def on_available_webview_title(self, webview, gparamstring):
        self.set_title(webview.get_title())

    def on_new_webview_popup(self, webview, navigation_action):
        """Handles web popups created by this dialog's webview"""
        uri = navigation_action.get_request().get_uri()
        view = WebKit2.WebView.new_with_related_view(webview)
        view.load_uri(uri)
        dialog = WebPopupDialog(view, parent=self)
        dialog.set_modal(True)
        dialog.show()
        return view

    def on_webview_close(self, webview):
        self.destroy()
__init__(self, webview, parent=None) special
Source code in lutris/gui/dialogs/webconnect_dialog.py
def __init__(self, webview, parent=None):
    # pylint: disable=no-member
    self.parent = parent
    super().__init__(title=_('Loading...'), parent=parent)
    self.webview = webview
    self.webview.connect("ready-to-show", self.on_ready_webview)
    self.webview.connect("notify::title", self.on_available_webview_title)
    self.webview.connect("create", self.on_new_webview_popup)
    self.webview.connect("close", self.on_webview_close)
    self.vbox.pack_start(self.webview, True, True, 0)
    self.set_border_width(0)
    self.set_default_size(390, 500)
on_available_webview_title(self, webview, gparamstring)
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_available_webview_title(self, webview, gparamstring):
    self.set_title(webview.get_title())
on_new_webview_popup(self, webview, navigation_action)

Handles web popups created by this dialog's webview

Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_new_webview_popup(self, webview, navigation_action):
    """Handles web popups created by this dialog's webview"""
    uri = navigation_action.get_request().get_uri()
    view = WebKit2.WebView.new_with_related_view(webview)
    view.load_uri(uri)
    dialog = WebPopupDialog(view, parent=self)
    dialog.set_modal(True)
    dialog.show()
    return view
on_ready_webview(self, webview)
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_ready_webview(self, webview):
    self.show_all()
on_webview_close(self, webview)
Source code in lutris/gui/dialogs/webconnect_dialog.py
def on_webview_close(self, webview):
    self.destroy()

installer special

file_box

Widgets for the installer window

InstallerFileBox (VBox)

Container for an installer file downloader / selector

Source code in lutris/gui/installer/file_box.py
class InstallerFileBox(Gtk.VBox):
    """Container for an installer file downloader / selector"""

    __gsignals__ = {
        "file-available": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "file-ready": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "file-unready": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, installer_file):
        super().__init__()
        self.installer_file = installer_file
        self.cache_to_pga = self.installer_file.uses_pga_cache()
        self.started = False
        self.start_func = None
        self.stop_func = None
        self.state_label = None  # Use this label to display status update
        self.set_margin_left(12)
        self.set_margin_right(12)
        self.provider = self.installer_file.provider
        self.file_provider_widget = None
        self.add(self.get_widgets())

    @property
    def is_ready(self):
        """Whether the file is ready to be downloaded / fetched from its provider"""
        if (
                self.provider in ("user", "pga")
                and not system.path_exists(self.installer_file.dest_file)
        ):
            return False
        return True

    def get_download_progress(self):
        """Return the widget for the download progress bar"""
        download_progress = DownloadProgressBox({
            "url": self.installer_file.url,
            "dest": self.installer_file.dest_file,
            "referer": self.installer_file.referer
        })
        download_progress.connect("complete", self.on_download_complete)
        download_progress.connect("cancel", self.on_download_cancelled)
        download_progress.show()
        if (
                not self.installer_file.uses_pga_cache()
                and system.path_exists(self.installer_file.dest_file)
        ):
            os.remove(self.installer_file.dest_file)
        return download_progress

    def get_file_provider_widget(self):
        """Return the widget used to track progress of file"""
        box = Gtk.VBox(spacing=6)
        if self.provider == "download":
            download_progress = self.get_download_progress()
            self.start_func = download_progress.start
            self.stop_func = download_progress.on_cancel_clicked
            box.pack_start(download_progress, False, False, 0)
            return box
        if self.provider == "pga":
            url_label = InstallerLabel("In cache: %s" % self.get_file_label(), wrap=False)
            box.pack_start(url_label, False, False, 6)
            return box
        if self.provider == "user":
            user_label = InstallerLabel(gtk_safe(self.installer_file.human_url))
            box.pack_start(user_label, False, False, 0)
            return box
        if self.provider == "steam":
            steam_installer = SteamInstaller(self.installer_file.url,
                                             self.installer_file.id)
            steam_installer.connect("steam-game-installed", self.on_download_complete)
            steam_installer.connect("steam-state-changed", self.on_state_changed)
            self.start_func = steam_installer.install_steam_game
            self.stop_func = steam_installer.stop_func

            steam_box = Gtk.HBox(spacing=6)
            info_box = Gtk.VBox(spacing=6)
            steam_label = InstallerLabel(_("Steam game <b>{appid}</b>").format(
                appid=steam_installer.appid
            ))
            info_box.add(steam_label)
            self.state_label = InstallerLabel("")
            info_box.add(self.state_label)
            steam_box.add(info_box)
            return steam_box
        raise ValueError("Invalid provider %s" % self.provider)

    def get_file_label(self):
        """Return a human readable label for installer files"""
        url = self.installer_file.url
        if url.startswith("http"):
            parsed = urlparse(url)
            label = _("{file} on {host}").format(file=self.installer_file.filename, host=parsed.netloc)
        elif url.startswith("N/A"):
            label = url[3:].lstrip(":")
        else:
            label = url
        return add_url_tags(gtk_safe(label))

    def get_combobox_model(self):
        """"Return the combobox's model"""
        model = Gtk.ListStore(str, str)
        if "download" in self.installer_file.providers:
            model.append(["download", "Download"])
        if "pga" in self.installer_file.providers:
            model.append(["pga", "Use Cache"])
        if "steam" in self.installer_file.providers:
            model.append(["steam", "Steam"])
        model.append(["user", "Select File"])
        return model

    def get_combobox(self):
        """Return the combobox widget to select file source"""
        combobox = Gtk.ComboBox.new_with_model(self.get_combobox_model())
        combobox.set_id_column(0)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.connect("changed", self.on_source_changed)
        combobox.set_active_id(self.provider)
        return combobox

    def replace_file_provider_widget(self):
        """Replace the file provider label and the source button with the actual widget"""
        self.file_provider_widget.destroy()
        widget_box = self.get_children()[0]
        if self.started:
            self.file_provider_widget = self.get_file_provider_widget()
            # Also remove the the source button
            for child in widget_box.get_children():
                child.destroy()
        else:
            self.file_provider_widget = self.get_file_provider_label()
        widget_box.pack_start(self.file_provider_widget, True, True, 0)
        widget_box.reorder_child(self.file_provider_widget, 0)
        widget_box.show_all()

    def on_source_changed(self, combobox):
        """Change the source to a new provider, emit a new state"""
        tree_iter = combobox.get_active_iter()
        if tree_iter is None:
            return
        model = combobox.get_model()
        source = model[tree_iter][0]
        if source == self.provider:
            return
        self.provider = source
        self.replace_file_provider_widget()
        if self.provider == "user":
            self.emit("file-unready")
        else:
            self.emit("file-ready")

    def get_file_provider_label(self):
        """Return the label displayed before the download starts"""
        if self.provider == "user":
            box = Gtk.VBox(spacing=6)
            label = InstallerLabel(self.get_file_label())
            label.props.can_focus = True
            box.pack_start(label, False, False, 0)
            location_entry = FileChooserEntry(
                self.installer_file.human_url,
                Gtk.FileChooserAction.OPEN,
                path=None
            )
            location_entry.entry.connect("changed", self.on_location_changed)
            location_entry.show()
            box.pack_start(location_entry, False, False, 0)
            if self.installer_file.uses_pga_cache(create=True):
                cache_option = Gtk.CheckButton(_("Cache file for future installations"))
                cache_option.set_active(self.cache_to_pga)
                cache_option.connect("toggled", self.on_user_file_cached)
                box.pack_start(cache_option, False, False, 0)
            return box
        return InstallerLabel(self.get_file_label())

    def get_widgets(self):
        """Return the widget with the source of the file and a way to change its source"""
        box = Gtk.HBox(
            spacing=12,
            margin_top=6,
            margin_bottom=6
        )
        self.file_provider_widget = self.get_file_provider_label()
        box.pack_start(self.file_provider_widget, True, True, 0)
        source_box = Gtk.HBox()
        source_box.props.valign = Gtk.Align.START
        box.pack_start(source_box, False, False, 0)
        source_box.pack_start(InstallerLabel(_("Source:")), False, False, 0)
        combobox = self.get_combobox()
        source_box.pack_start(combobox, False, False, 0)
        return box

    def on_location_changed(self, widget):
        """Open a file picker when the browse button is clicked"""
        file_path = os.path.expanduser(widget.get_text())
        self.installer_file.dest_file = file_path
        if system.path_exists(file_path):
            self.emit("file-ready")
        else:
            self.emit("file-unready")

    def on_user_file_cached(self, checkbutton):
        """Enable or disable caching of user provided files"""
        self.cache_to_pga = checkbutton.get_active()

    def on_state_changed(self, _widget, state):
        """Update the state label with a new state"""
        self.state_label.set_text(state)

    def start(self):
        """Starts the download of the file"""
        self.started = True
        self.installer_file.prepare()
        self.replace_file_provider_widget()
        if self.provider in ("pga", "user") and self.is_ready:
            self.emit("file-available")
            self.cache_file()
            return
        if self.start_func:
            return self.start_func()

    def cache_file(self):
        """Copy file to the PGA cache"""
        if self.cache_to_pga:
            save_to_cache(self.installer_file.dest_file, self.installer_file.cache_path)

    def on_download_cancelled(self, downloader):
        """Handle cancellation of installers"""
        logger.error("Download from %s cancelled", downloader)
        downloader.set_retry_button()

    def on_download_complete(self, widget, _data=None):
        """Action called on a completed download."""
        logger.info("Download completed")
        if isinstance(widget, SteamInstaller):
            self.installer_file.dest_file = widget.get_steam_data_path()
        else:
            self.cache_file()
        self.emit("file-available")
is_ready property readonly

Whether the file is ready to be downloaded / fetched from its provider

__init__(self, installer_file) special
Source code in lutris/gui/installer/file_box.py
def __init__(self, installer_file):
    super().__init__()
    self.installer_file = installer_file
    self.cache_to_pga = self.installer_file.uses_pga_cache()
    self.started = False
    self.start_func = None
    self.stop_func = None
    self.state_label = None  # Use this label to display status update
    self.set_margin_left(12)
    self.set_margin_right(12)
    self.provider = self.installer_file.provider
    self.file_provider_widget = None
    self.add(self.get_widgets())
cache_file(self)

Copy file to the PGA cache

Source code in lutris/gui/installer/file_box.py
def cache_file(self):
    """Copy file to the PGA cache"""
    if self.cache_to_pga:
        save_to_cache(self.installer_file.dest_file, self.installer_file.cache_path)
get_combobox(self)

Return the combobox widget to select file source

Source code in lutris/gui/installer/file_box.py
def get_combobox(self):
    """Return the combobox widget to select file source"""
    combobox = Gtk.ComboBox.new_with_model(self.get_combobox_model())
    combobox.set_id_column(0)
    renderer_text = Gtk.CellRendererText()
    combobox.pack_start(renderer_text, True)
    combobox.add_attribute(renderer_text, "text", 1)
    combobox.connect("changed", self.on_source_changed)
    combobox.set_active_id(self.provider)
    return combobox
get_combobox_model(self)

"Return the combobox's model

Source code in lutris/gui/installer/file_box.py
def get_combobox_model(self):
    """"Return the combobox's model"""
    model = Gtk.ListStore(str, str)
    if "download" in self.installer_file.providers:
        model.append(["download", "Download"])
    if "pga" in self.installer_file.providers:
        model.append(["pga", "Use Cache"])
    if "steam" in self.installer_file.providers:
        model.append(["steam", "Steam"])
    model.append(["user", "Select File"])
    return model
get_download_progress(self)

Return the widget for the download progress bar

Source code in lutris/gui/installer/file_box.py
def get_download_progress(self):
    """Return the widget for the download progress bar"""
    download_progress = DownloadProgressBox({
        "url": self.installer_file.url,
        "dest": self.installer_file.dest_file,
        "referer": self.installer_file.referer
    })
    download_progress.connect("complete", self.on_download_complete)
    download_progress.connect("cancel", self.on_download_cancelled)
    download_progress.show()
    if (
            not self.installer_file.uses_pga_cache()
            and system.path_exists(self.installer_file.dest_file)
    ):
        os.remove(self.installer_file.dest_file)
    return download_progress
get_file_label(self)

Return a human readable label for installer files

Source code in lutris/gui/installer/file_box.py
def get_file_label(self):
    """Return a human readable label for installer files"""
    url = self.installer_file.url
    if url.startswith("http"):
        parsed = urlparse(url)
        label = _("{file} on {host}").format(file=self.installer_file.filename, host=parsed.netloc)
    elif url.startswith("N/A"):
        label = url[3:].lstrip(":")
    else:
        label = url
    return add_url_tags(gtk_safe(label))
get_file_provider_label(self)

Return the label displayed before the download starts

Source code in lutris/gui/installer/file_box.py
def get_file_provider_label(self):
    """Return the label displayed before the download starts"""
    if self.provider == "user":
        box = Gtk.VBox(spacing=6)
        label = InstallerLabel(self.get_file_label())
        label.props.can_focus = True
        box.pack_start(label, False, False, 0)
        location_entry = FileChooserEntry(
            self.installer_file.human_url,
            Gtk.FileChooserAction.OPEN,
            path=None
        )
        location_entry.entry.connect("changed", self.on_location_changed)
        location_entry.show()
        box.pack_start(location_entry, False, False, 0)
        if self.installer_file.uses_pga_cache(create=True):
            cache_option = Gtk.CheckButton(_("Cache file for future installations"))
            cache_option.set_active(self.cache_to_pga)
            cache_option.connect("toggled", self.on_user_file_cached)
            box.pack_start(cache_option, False, False, 0)
        return box
    return InstallerLabel(self.get_file_label())
get_file_provider_widget(self)

Return the widget used to track progress of file

Source code in lutris/gui/installer/file_box.py
def get_file_provider_widget(self):
    """Return the widget used to track progress of file"""
    box = Gtk.VBox(spacing=6)
    if self.provider == "download":
        download_progress = self.get_download_progress()
        self.start_func = download_progress.start
        self.stop_func = download_progress.on_cancel_clicked
        box.pack_start(download_progress, False, False, 0)
        return box
    if self.provider == "pga":
        url_label = InstallerLabel("In cache: %s" % self.get_file_label(), wrap=False)
        box.pack_start(url_label, False, False, 6)
        return box
    if self.provider == "user":
        user_label = InstallerLabel(gtk_safe(self.installer_file.human_url))
        box.pack_start(user_label, False, False, 0)
        return box
    if self.provider == "steam":
        steam_installer = SteamInstaller(self.installer_file.url,
                                         self.installer_file.id)
        steam_installer.connect("steam-game-installed", self.on_download_complete)
        steam_installer.connect("steam-state-changed", self.on_state_changed)
        self.start_func = steam_installer.install_steam_game
        self.stop_func = steam_installer.stop_func

        steam_box = Gtk.HBox(spacing=6)
        info_box = Gtk.VBox(spacing=6)
        steam_label = InstallerLabel(_("Steam game <b>{appid}</b>").format(
            appid=steam_installer.appid
        ))
        info_box.add(steam_label)
        self.state_label = InstallerLabel("")
        info_box.add(self.state_label)
        steam_box.add(info_box)
        return steam_box
    raise ValueError("Invalid provider %s" % self.provider)
get_widgets(self)

Return the widget with the source of the file and a way to change its source

Source code in lutris/gui/installer/file_box.py
def get_widgets(self):
    """Return the widget with the source of the file and a way to change its source"""
    box = Gtk.HBox(
        spacing=12,
        margin_top=6,
        margin_bottom=6
    )
    self.file_provider_widget = self.get_file_provider_label()
    box.pack_start(self.file_provider_widget, True, True, 0)
    source_box = Gtk.HBox()
    source_box.props.valign = Gtk.Align.START
    box.pack_start(source_box, False, False, 0)
    source_box.pack_start(InstallerLabel(_("Source:")), False, False, 0)
    combobox = self.get_combobox()
    source_box.pack_start(combobox, False, False, 0)
    return box
on_download_cancelled(self, downloader)

Handle cancellation of installers

Source code in lutris/gui/installer/file_box.py
def on_download_cancelled(self, downloader):
    """Handle cancellation of installers"""
    logger.error("Download from %s cancelled", downloader)
    downloader.set_retry_button()
on_download_complete(self, widget, _data=None)

Action called on a completed download.

Source code in lutris/gui/installer/file_box.py
def on_download_complete(self, widget, _data=None):
    """Action called on a completed download."""
    logger.info("Download completed")
    if isinstance(widget, SteamInstaller):
        self.installer_file.dest_file = widget.get_steam_data_path()
    else:
        self.cache_file()
    self.emit("file-available")
on_location_changed(self, widget)

Open a file picker when the browse button is clicked

Source code in lutris/gui/installer/file_box.py
def on_location_changed(self, widget):
    """Open a file picker when the browse button is clicked"""
    file_path = os.path.expanduser(widget.get_text())
    self.installer_file.dest_file = file_path
    if system.path_exists(file_path):
        self.emit("file-ready")
    else:
        self.emit("file-unready")
on_source_changed(self, combobox)

Change the source to a new provider, emit a new state

Source code in lutris/gui/installer/file_box.py
def on_source_changed(self, combobox):
    """Change the source to a new provider, emit a new state"""
    tree_iter = combobox.get_active_iter()
    if tree_iter is None:
        return
    model = combobox.get_model()
    source = model[tree_iter][0]
    if source == self.provider:
        return
    self.provider = source
    self.replace_file_provider_widget()
    if self.provider == "user":
        self.emit("file-unready")
    else:
        self.emit("file-ready")
on_state_changed(self, _widget, state)

Update the state label with a new state

Source code in lutris/gui/installer/file_box.py
def on_state_changed(self, _widget, state):
    """Update the state label with a new state"""
    self.state_label.set_text(state)
on_user_file_cached(self, checkbutton)

Enable or disable caching of user provided files

Source code in lutris/gui/installer/file_box.py
def on_user_file_cached(self, checkbutton):
    """Enable or disable caching of user provided files"""
    self.cache_to_pga = checkbutton.get_active()
replace_file_provider_widget(self)

Replace the file provider label and the source button with the actual widget

Source code in lutris/gui/installer/file_box.py
def replace_file_provider_widget(self):
    """Replace the file provider label and the source button with the actual widget"""
    self.file_provider_widget.destroy()
    widget_box = self.get_children()[0]
    if self.started:
        self.file_provider_widget = self.get_file_provider_widget()
        # Also remove the the source button
        for child in widget_box.get_children():
            child.destroy()
    else:
        self.file_provider_widget = self.get_file_provider_label()
    widget_box.pack_start(self.file_provider_widget, True, True, 0)
    widget_box.reorder_child(self.file_provider_widget, 0)
    widget_box.show_all()
start(self)

Starts the download of the file

Source code in lutris/gui/installer/file_box.py
def start(self):
    """Starts the download of the file"""
    self.started = True
    self.installer_file.prepare()
    self.replace_file_provider_widget()
    if self.provider in ("pga", "user") and self.is_ready:
        self.emit("file-available")
        self.cache_file()
        return
    if self.start_func:
        return self.start_func()

files_box

InstallerFilesBox (ListBox)

List box presenting all files needed for an installer

Source code in lutris/gui/installer/files_box.py
class InstallerFilesBox(Gtk.ListBox):
    """List box presenting all files needed for an installer"""

    max_downloads = 3

    __gsignals__ = {
        "files-ready": (GObject.SIGNAL_RUN_LAST, None, (bool, )),
        "files-available": (GObject.SIGNAL_RUN_LAST, None, ())
    }

    def __init__(self, installer, parent):
        super().__init__()
        self.parent = parent
        self.installer = installer
        self.installer_files = installer.files
        self.ready_files = set()
        self.available_files = set()
        self.installer_files_boxes = {}
        self._file_queue = []
        for installer_file in installer.files:
            installer_file_box = InstallerFileBox(installer_file)
            installer_file_box.connect("file-ready", self.on_file_ready)
            installer_file_box.connect("file-unready", self.on_file_unready)
            installer_file_box.connect("file-available", self.on_file_available)
            self.installer_files_boxes[installer_file.id] = installer_file_box
            self.add(installer_file_box)
            if installer_file_box.is_ready:
                self.ready_files.add(installer_file.id)
        self.show_all()
        self.check_files_ready()

    def start_all(self):
        """Iterates through installer files while keeping the number
        of simultaneously downloaded files down to a maximum number"""
        started_downloads = 0
        for file_id, file_entry in self.installer_files_boxes.items():
            if file_entry.provider == "download":
                started_downloads += 1
                if started_downloads <= self.max_downloads:
                    file_entry.start()
                else:
                    self._file_queue.append(file_id)
            else:
                file_entry.start()

    def stop_all(self):
        """Stops all ongoing files gathering.
        Iterates through installer files, and call the "stop" command
        if they've been started and not available yet.
        """
        self._file_queue.clear()
        for file_id, file_box in self.installer_files_boxes.items():
            if file_box.started and file_id not in self.available_files and file_box.stop_func is not None:
                file_box.stop_func()

    @property
    def is_ready(self):
        """Return True if all files are ready to be fetched"""
        return len(self.ready_files) == len(self.installer.files)

    def check_files_ready(self):
        """Checks if all installer files are ready and emit a signal if so"""
        if self.is_ready:
            self.emit("files-ready", self.is_ready)
        else:
            logger.info("Waiting for user to provide files")

    def on_file_ready(self, widget):
        """Fired when a file has a valid provider.
        If the file is user provided, it must set to a valid path.
        """
        file_id = widget.installer_file.id
        self.ready_files.add(file_id)
        self.check_files_ready()

    def on_file_unready(self, widget):
        """Fired when a file can't be provided.
        Blocks the installer from continuing.
        """
        file_id = widget.installer_file.id
        self.ready_files.remove(file_id)
        self.check_files_ready()

    def on_file_available(self, widget):
        """A new file is available"""
        file_id = widget.installer_file.id
        logger.debug("%s is available", file_id)
        self.available_files.add(file_id)
        if self._file_queue:
            next_file_id = self._file_queue.pop()
            self.installer_files_boxes[next_file_id].start()
        if len(self.available_files) == len(self.installer_files):
            logger.info("All files available")
            self.emit("files-available")

    def get_game_files(self):
        """Return a mapping of the local files usable by the interpreter"""
        return {
            installer_file.id: installer_file.dest_file
            for installer_file in self.installer_files
        }
is_ready property readonly

Return True if all files are ready to be fetched

max_downloads
__init__(self, installer, parent) special
Source code in lutris/gui/installer/files_box.py
def __init__(self, installer, parent):
    super().__init__()
    self.parent = parent
    self.installer = installer
    self.installer_files = installer.files
    self.ready_files = set()
    self.available_files = set()
    self.installer_files_boxes = {}
    self._file_queue = []
    for installer_file in installer.files:
        installer_file_box = InstallerFileBox(installer_file)
        installer_file_box.connect("file-ready", self.on_file_ready)
        installer_file_box.connect("file-unready", self.on_file_unready)
        installer_file_box.connect("file-available", self.on_file_available)
        self.installer_files_boxes[installer_file.id] = installer_file_box
        self.add(installer_file_box)
        if installer_file_box.is_ready:
            self.ready_files.add(installer_file.id)
    self.show_all()
    self.check_files_ready()
check_files_ready(self)

Checks if all installer files are ready and emit a signal if so

Source code in lutris/gui/installer/files_box.py
def check_files_ready(self):
    """Checks if all installer files are ready and emit a signal if so"""
    if self.is_ready:
        self.emit("files-ready", self.is_ready)
    else:
        logger.info("Waiting for user to provide files")
get_game_files(self)

Return a mapping of the local files usable by the interpreter

Source code in lutris/gui/installer/files_box.py
def get_game_files(self):
    """Return a mapping of the local files usable by the interpreter"""
    return {
        installer_file.id: installer_file.dest_file
        for installer_file in self.installer_files
    }
on_file_available(self, widget)

A new file is available

Source code in lutris/gui/installer/files_box.py
def on_file_available(self, widget):
    """A new file is available"""
    file_id = widget.installer_file.id
    logger.debug("%s is available", file_id)
    self.available_files.add(file_id)
    if self._file_queue:
        next_file_id = self._file_queue.pop()
        self.installer_files_boxes[next_file_id].start()
    if len(self.available_files) == len(self.installer_files):
        logger.info("All files available")
        self.emit("files-available")
on_file_ready(self, widget)

Fired when a file has a valid provider. If the file is user provided, it must set to a valid path.

Source code in lutris/gui/installer/files_box.py
def on_file_ready(self, widget):
    """Fired when a file has a valid provider.
    If the file is user provided, it must set to a valid path.
    """
    file_id = widget.installer_file.id
    self.ready_files.add(file_id)
    self.check_files_ready()
on_file_unready(self, widget)

Fired when a file can't be provided. Blocks the installer from continuing.

Source code in lutris/gui/installer/files_box.py
def on_file_unready(self, widget):
    """Fired when a file can't be provided.
    Blocks the installer from continuing.
    """
    file_id = widget.installer_file.id
    self.ready_files.remove(file_id)
    self.check_files_ready()
start_all(self)

Iterates through installer files while keeping the number of simultaneously downloaded files down to a maximum number

Source code in lutris/gui/installer/files_box.py
def start_all(self):
    """Iterates through installer files while keeping the number
    of simultaneously downloaded files down to a maximum number"""
    started_downloads = 0
    for file_id, file_entry in self.installer_files_boxes.items():
        if file_entry.provider == "download":
            started_downloads += 1
            if started_downloads <= self.max_downloads:
                file_entry.start()
            else:
                self._file_queue.append(file_id)
        else:
            file_entry.start()
stop_all(self)

Stops all ongoing files gathering. Iterates through installer files, and call the "stop" command if they've been started and not available yet.

Source code in lutris/gui/installer/files_box.py
def stop_all(self):
    """Stops all ongoing files gathering.
    Iterates through installer files, and call the "stop" command
    if they've been started and not available yet.
    """
    self._file_queue.clear()
    for file_id, file_box in self.installer_files_boxes.items():
        if file_box.started and file_id not in self.available_files and file_box.stop_func is not None:
            file_box.stop_func()

script_box

InstallerScriptBox (VBox)

Box displaying the details of a script, with associated action buttons

Source code in lutris/gui/installer/script_box.py
class InstallerScriptBox(Gtk.VBox):
    """Box displaying the details of a script, with associated action buttons"""

    def __init__(self, script, parent=None, revealed=False):
        super().__init__()
        self.script = script
        self.parent = parent
        self.revealer = None
        self.set_margin_left(12)
        self.set_margin_right(12)
        box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
        box.pack_start(self.get_infobox(), True, True, 0)
        box.add(self.get_install_button())
        self.add(box)
        self.add(self.get_revealer(revealed))

    def get_rating(self):
        """Return a string representation of the API rating"""
        return ""

    def get_infobox(self):
        """Return the central information box"""
        info_box = Gtk.VBox(spacing=6)
        title_box = Gtk.HBox(spacing=6)
        runner_label = InstallerLabel("%s" % self.script["runner"])
        runner_label.get_style_context().add_class("info-pill")
        title_box.pack_start(runner_label, False, False, 0)
        title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
        title_box.pack_start(InstallerLabel(""), True, True, 0)
        rating_label = InstallerLabel(self.get_rating())
        rating_label.set_alignment(1, 0.5)
        title_box.pack_end(rating_label, False, False, 0)
        info_box.add(title_box)
        info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
        return info_box

    def get_revealer(self, revealed):
        """Return the revelaer widget"""
        self.revealer = Gtk.Revealer()
        self.revealer.add(self.get_notes())
        self.revealer.set_reveal_child(revealed)
        return self.revealer

    def get_install_button(self):
        """Return the install button widget"""
        align = Gtk.Alignment()
        align.set(0, 0, 0, 0)

        install_button = Gtk.Button(_("Install"))
        install_button.connect("clicked", self.on_install_clicked)
        align.add(install_button)
        return align

    def get_notes(self):
        """Return the notes widget"""
        notes = self.script["notes"].strip()
        if not notes:
            return Gtk.Alignment()
        notes_label = InstallerLabel(notes)
        notes_label.set_margin_top(12)
        notes_label.set_margin_bottom(12)
        notes_label.set_margin_right(12)
        notes_label.set_margin_left(12)
        return notes_label

    def reveal(self, reveal=True):
        """Show or hide the information in the revealer"""
        if self.revealer:
            self.revealer.set_reveal_child(reveal)

    def on_install_clicked(self, _widget):
        """Handler to notify the parent of the selected installer"""
        self.parent.emit("installer-selected", self.script["version"])
__init__(self, script, parent=None, revealed=False) special
Source code in lutris/gui/installer/script_box.py
def __init__(self, script, parent=None, revealed=False):
    super().__init__()
    self.script = script
    self.parent = parent
    self.revealer = None
    self.set_margin_left(12)
    self.set_margin_right(12)
    box = Gtk.Box(spacing=12, margin_top=6, margin_bottom=6)
    box.pack_start(self.get_infobox(), True, True, 0)
    box.add(self.get_install_button())
    self.add(box)
    self.add(self.get_revealer(revealed))
get_infobox(self)

Return the central information box

Source code in lutris/gui/installer/script_box.py
def get_infobox(self):
    """Return the central information box"""
    info_box = Gtk.VBox(spacing=6)
    title_box = Gtk.HBox(spacing=6)
    runner_label = InstallerLabel("%s" % self.script["runner"])
    runner_label.get_style_context().add_class("info-pill")
    title_box.pack_start(runner_label, False, False, 0)
    title_box.add(InstallerLabel("<b>%s</b>" % gtk_safe(self.script["version"])))
    title_box.pack_start(InstallerLabel(""), True, True, 0)
    rating_label = InstallerLabel(self.get_rating())
    rating_label.set_alignment(1, 0.5)
    title_box.pack_end(rating_label, False, False, 0)
    info_box.add(title_box)
    info_box.add(InstallerLabel(add_url_tags(self.script["description"])))
    return info_box
get_install_button(self)

Return the install button widget

Source code in lutris/gui/installer/script_box.py
def get_install_button(self):
    """Return the install button widget"""
    align = Gtk.Alignment()
    align.set(0, 0, 0, 0)

    install_button = Gtk.Button(_("Install"))
    install_button.connect("clicked", self.on_install_clicked)
    align.add(install_button)
    return align
get_notes(self)

Return the notes widget

Source code in lutris/gui/installer/script_box.py
def get_notes(self):
    """Return the notes widget"""
    notes = self.script["notes"].strip()
    if not notes:
        return Gtk.Alignment()
    notes_label = InstallerLabel(notes)
    notes_label.set_margin_top(12)
    notes_label.set_margin_bottom(12)
    notes_label.set_margin_right(12)
    notes_label.set_margin_left(12)
    return notes_label
get_rating(self)

Return a string representation of the API rating

Source code in lutris/gui/installer/script_box.py
def get_rating(self):
    """Return a string representation of the API rating"""
    return ""
get_revealer(self, revealed)

Return the revelaer widget

Source code in lutris/gui/installer/script_box.py
def get_revealer(self, revealed):
    """Return the revelaer widget"""
    self.revealer = Gtk.Revealer()
    self.revealer.add(self.get_notes())
    self.revealer.set_reveal_child(revealed)
    return self.revealer
on_install_clicked(self, _widget)

Handler to notify the parent of the selected installer

Source code in lutris/gui/installer/script_box.py
def on_install_clicked(self, _widget):
    """Handler to notify the parent of the selected installer"""
    self.parent.emit("installer-selected", self.script["version"])
reveal(self, reveal=True)

Show or hide the information in the revealer

Source code in lutris/gui/installer/script_box.py
def reveal(self, reveal=True):
    """Show or hide the information in the revealer"""
    if self.revealer:
        self.revealer.set_reveal_child(reveal)

script_picker

InstallerPicker (ListBox)

List box to pick between several installers

Source code in lutris/gui/installer/script_picker.py
class InstallerPicker(Gtk.ListBox):
    """List box to pick between several installers"""

    __gsignals__ = {"installer-selected": (GObject.SIGNAL_RUN_FIRST, None, (str, ))}

    def __init__(self, scripts):
        super().__init__()
        revealed = True
        for script in scripts:
            self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
            revealed = False  # Only reveal the first installer.
        self.connect('row-selected', self.on_activate)
        self.show_all()

    @staticmethod
    def on_activate(widget, row):
        """Handler for hiding and showing the revealers in children"""
        for script_box_row in widget:
            script_box = script_box_row.get_children()[0]
            script_box.reveal(False)
        installer_row = row.get_children()[0]
        installer_row.reveal()
__init__(self, scripts) special
Source code in lutris/gui/installer/script_picker.py
def __init__(self, scripts):
    super().__init__()
    revealed = True
    for script in scripts:
        self.add(InstallerScriptBox(script, parent=self, revealed=revealed))
        revealed = False  # Only reveal the first installer.
    self.connect('row-selected', self.on_activate)
    self.show_all()
on_activate(widget, row) staticmethod

Handler for hiding and showing the revealers in children

Source code in lutris/gui/installer/script_picker.py
@staticmethod
def on_activate(widget, row):
    """Handler for hiding and showing the revealers in children"""
    for script_box_row in widget:
        script_box = script_box_row.get_children()[0]
        script_box.reveal(False)
    installer_row = row.get_children()[0]
    installer_row.reveal()

widgets

InstallerLabel (Label)

A label for installers

Source code in lutris/gui/installer/widgets.py
class InstallerLabel(Gtk.Label):
    """A label for installers"""

    def __init__(self, text, wrap=True):
        super().__init__()
        if wrap:
            self.set_line_wrap(True)
            self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        else:
            self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
        self.set_alignment(0, 0.5)
        self.set_margin_right(12)
        self.set_markup(text)
        self.props.can_focus = False
        self.set_tooltip_text(text)
__init__(self, text, wrap=True) special
Source code in lutris/gui/installer/widgets.py
def __init__(self, text, wrap=True):
    super().__init__()
    if wrap:
        self.set_line_wrap(True)
        self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
    else:
        self.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
    self.set_alignment(0, 0.5)
    self.set_margin_right(12)
    self.set_markup(text)
    self.props.can_focus = False
    self.set_tooltip_text(text)

installerwindow

Window used for game installers

InstallerWindow (BaseApplicationWindow)

GUI for the install process.

Source code in lutris/gui/installerwindow.py
class InstallerWindow(BaseApplicationWindow):  # pylint: disable=too-many-public-methods
    """GUI for the install process."""

    def __init__(
        self,
        installers,
        service=None,
        appid=None,
        application=None,
        is_update=False
    ):
        super().__init__(application=application)
        self.set_default_size(540, 320)
        self.installers = installers
        self.config = {}
        self.service = service
        self.appid = appid
        self.install_in_progress = False
        self.interpreter = None
        self.is_update = is_update
        self.log_buffer = None
        self.log_textview = None

        self._cancel_files_func = None

        self.title_label = InstallerLabel()
        self.title_label.set_selectable(False)
        self.vbox.add(self.title_label)

        self.status_label = InstallerLabel()
        self.status_label.set_max_width_chars(80)
        self.status_label.set_property("wrap", True)
        self.status_label.set_selectable(True)
        self.vbox.add(self.status_label)

        self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.vbox.pack_start(self.widget_box, True, True, 0)

        self.vbox.add(Gtk.HSeparator())

        button_box = Gtk.Box()
        self.cache_button = Gtk.Button(_("Cache"))
        self.cache_button.connect("clicked", self.on_cache_clicked)
        button_box.add(self.cache_button)

        self.action_buttons = Gtk.Box(spacing=6)
        action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
        action_buttons_alignment.add(self.action_buttons)
        button_box.pack_end(action_buttons_alignment, True, True, 0)
        self.vbox.pack_start(button_box, False, True, 0)

        self.cancel_button = self.add_button(
            _("C_ancel"), self.confirm_cancel, tooltip=_("Abort and revert the installation")
        )
        self.eject_button = self.add_button(_("_Eject"), self.on_eject_clicked)
        self.source_button = self.add_button(_("_View source"), self.on_source_clicked)
        self.install_button = self.add_button(_("_Install"), self.on_install_clicked)
        self.continue_button = self.add_button(_("_Continue"))
        self.play_button = self.add_button(_("_Launch"), self.launch_game)
        self.close_button = self.add_button(_("_Close"), self.on_destroy)

        self.continue_handler = None

        self.clean_widgets()
        self.show_all()
        self.close_button.hide()
        self.play_button.hide()
        self.install_button.hide()
        self.source_button.hide()
        self.eject_button.hide()
        self.continue_button.hide()
        self.install_in_progress = True
        self.widget_box.show()
        self.title_label.show()
        self.choose_installer()

        self.present()

    def add_button(self, label, handler=None, tooltip=None):
        """Add a button to the action buttons box"""
        button = Gtk.Button.new_with_mnemonic(label)
        if tooltip:
            button.set_tooltip_text(tooltip)
        if handler:
            button.connect("clicked", handler)
        self.action_buttons.add(button)
        return button

    def validate_scripts(self):
        """Auto-fixes some script aspects and checks for mandatory fields"""
        if not self.installers:
            raise ScriptingError("No installer available")
        for script in self.installers:
            for item in ["description", "notes"]:
                script[item] = script.get(item) or ""
            for item in ["name", "runner", "version"]:
                if item not in script:
                    logger.error("Invalid script: %s", script)
                    raise ScriptingError(_('Missing field "%s" in install script') % item)

    def choose_installer(self):
        """Stage where we choose an install script."""
        self.validate_scripts()
        base_script = self.installers[0]
        self.title_label.set_markup(_("<b>Install %s</b>") % gtk_safe(base_script["name"]))
        installer_picker = InstallerPicker(self.installers)
        installer_picker.connect("installer-selected", self.on_installer_selected)
        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            child=installer_picker,
            visible=True
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)

    def on_cache_clicked(self, _button):
        """Open the cache configuration dialog"""
        CacheConfigurationDialog()

    def on_installer_selected(self, _widget, installer_version):
        """Sets the script interpreter to the correct script then proceed to
        install folder selection.

        If the installed game depends on another one and it's not installed,
        prompt the user to install it and quit this installer.
        """
        self.clean_widgets()
        try:
            script = None
            for _script in self.installers:
                if _script["version"] == installer_version:
                    script = _script
            self.interpreter = interpreter.ScriptInterpreter(script, self)

        except MissingGameDependency as ex:
            dlg = QuestionDialog(
                {
                    "question": _("This game requires %s. Do you want to install it?") % ex.slug,
                    "title": _("Missing dependency"),
                }
            )
            if dlg.result == Gtk.ResponseType.YES:
                InstallerWindow(
                    installers=self.installers,
                    service=self.service,
                    appid=self.appid,
                    application=self.application,
                )
            self.destroy()
            return
        self.title_label.set_markup(_("<b>Installing {}</b>").format(gtk_safe(self.interpreter.installer.game_name)))
        self.select_install_folder()

        desktop_shortcut_button = Gtk.CheckButton(_("Create desktop shortcut"), visible=True)
        desktop_shortcut_button.connect("clicked", self.on_create_desktop_shortcut_clicked)
        self.widget_box.pack_start(desktop_shortcut_button, False, False, 5)

        menu_shortcut_button = Gtk.CheckButton(_("Create application menu shortcut"), visible=True)
        menu_shortcut_button.connect("clicked", self.on_create_menu_shortcut_clicked)
        self.widget_box.pack_start(menu_shortcut_button, False, False, 5)

    def select_install_folder(self):
        """Stage where we select the install directory."""
        if not self.interpreter.installer.creates_game_folder:
            self.on_install_clicked(self.install_button)
            return
        self.set_message(_("Select installation directory"))
        default_path = self.interpreter.get_default_target()
        self.set_install_destination(default_path)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_button.hide()
        self.source_button.show()
        self.install_button.grab_focus()
        self.install_button.show()
        # self.manual_button.hide()

    def on_target_changed(self, text_entry, _data=None):
        """Set the installation target for the game."""
        self.interpreter.target_path = os.path.expanduser(text_entry.get_text())

    def on_install_clicked(self, button):
        """Let the interpreter take charge of the next stages."""
        button.hide()
        self.source_button.hide()
        self.interpreter.connect("runners-installed", self.on_runners_ready)
        GLib.idle_add(self.interpreter.launch_install)

    def set_install_destination(self, default_path=None):
        """Display the destination chooser."""
        self.install_button.set_visible(False)
        self.continue_button.show()
        self.continue_button.set_sensitive(False)
        location_entry = FileChooserEntry(
            "Select folder",
            Gtk.FileChooserAction.SELECT_FOLDER,
            path=default_path,
            warn_if_non_empty=True,
            warn_if_ntfs=True
        )
        location_entry.entry.connect("changed", self.on_target_changed)
        self.widget_box.pack_start(location_entry, False, False, 0)

    def ask_for_disc(self, message, callback, requires):
        """Ask the user to do insert a CD-ROM."""
        self.clean_widgets()
        label = InstallerLabel(message)
        label.show()
        self.widget_box.add(label)

        buttons_box = Gtk.Box()
        buttons_box.show()
        buttons_box.set_margin_top(40)
        buttons_box.set_margin_bottom(40)
        self.widget_box.add(buttons_box)

        autodetect_button = Gtk.Button(label=_("Autodetect"))
        autodetect_button.connect("clicked", callback, requires)
        autodetect_button.grab_focus()
        autodetect_button.show()
        buttons_box.pack_start(autodetect_button, True, True, 40)

        browse_button = Gtk.Button(label=_("Browse…"))
        callback_data = {"callback": callback, "requires": requires}
        browse_button.connect("clicked", self.on_browse_clicked, callback_data)
        browse_button.show()
        buttons_box.pack_start(browse_button, True, True, 40)

    def on_browse_clicked(self, widget, callback_data):
        dialog = DirectoryDialog(_("Select the folder where the disc is mounted"), parent=self)
        folder = dialog.folder
        callback = callback_data["callback"]
        requires = callback_data["requires"]
        callback(widget, requires, folder)

    def on_eject_clicked(self, _widget, data=None):
        self.interpreter.eject_wine_disc()

    def input_menu(self, alias, options, preselect, has_entry, callback):
        """Display an input request as a dropdown menu with options."""
        self.clean_widgets()

        model = Gtk.ListStore(str, str)
        for option in options:
            key, label = option.popitem()
            model.append([key, label])
        combobox = Gtk.ComboBox.new_with_model(model)
        renderer_text = Gtk.CellRendererText()
        combobox.pack_start(renderer_text, True)
        combobox.add_attribute(renderer_text, "text", 1)
        combobox.set_id_column(0)
        combobox.set_active_id(preselect)
        combobox.set_halign(Gtk.Align.CENTER)
        self.widget_box.pack_start(combobox, True, False, 100)

        combobox.connect("changed", self.on_input_menu_changed)
        combobox.show()
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_handler = self.continue_button.connect("clicked", callback, alias, combobox)
        self.continue_button.grab_focus()
        self.continue_button.show()
        self.on_input_menu_changed(combobox)

    def on_input_menu_changed(self, widget):
        """Enable continue button if a non-empty choice is selected"""
        self.continue_button.set_sensitive(bool(widget.get_active_id()))

    def on_runners_ready(self, _widget=None):
        """The runners are ready, proceed with file selection"""
        if self.interpreter.extras is None:
            extras = self.interpreter.get_extras()
            if extras:
                self.show_extras(extras)
                return
        try:
            patch_version = self.interpreter.installer.version if self.is_update else None
            self.interpreter.installer.prepare_game_files(patch_version)
        except UnavailableGame as ex:
            raise ScriptingError(str(ex)) from ex

        if not self.interpreter.installer.files:
            logger.debug("Installer doesn't require files")
            self.interpreter.launch_installer_commands()
            return
        self.show_installer_files_screen()

    def show_installer_files_screen(self):
        """Show installer screen with the file picker / downloader"""
        self.clean_widgets()
        self.set_status(_("Please review the files needed for the installation then click 'Continue'"))
        installer_files_box = InstallerFilesBox(self.interpreter.installer, self)
        installer_files_box.connect("files-available", self.on_files_available)
        installer_files_box.connect("files-ready", self.on_files_ready)
        self._cancel_files_func = installer_files_box.stop_all
        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            child=installer_files_box,
            visible=True
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)

        self.continue_button.show()
        self.continue_button.set_sensitive(installer_files_box.is_ready)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_handler = self.continue_button.connect(
            "clicked", self.on_files_confirmed, installer_files_box
        )

    def get_extra_label(self, extra):
        """Return a label for the extras picker"""
        label = extra["name"]
        _infos = []
        if extra.get("total_size"):
            _infos.append(human_size(extra["total_size"]))
        if extra.get("type"):
            _infos.append(extra["type"])
        if _infos:
            label += " (%s)" % ", ".join(_infos)
        return label

    def show_extras(self, all_extras):
        """Show installer screen with the extras picker"""
        self.clean_widgets()
        extra_treestore = Gtk.TreeStore(
            bool,  # is selected?
            bool,  # is inconsistent?
            str,   # id
            str,   # label
        )
        for extra_source, extras in all_extras.items():
            parent = extra_treestore.append(None, (None, None, None, extra_source))
            for extra in extras:
                extra_treestore.append(parent, (False, False, extra["id"], self.get_extra_label(extra)))

        treeview = Gtk.TreeView(extra_treestore)
        treeview.set_headers_visible(False)
        treeview.expand_all()
        renderer_toggle = Gtk.CellRendererToggle()
        renderer_toggle.connect("toggled", self.on_extra_toggled, extra_treestore)
        renderer_text = Gtk.CellRendererText()

        installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0, inconsistent=1)
        treeview.append_column(installed_column)

        label_column = Gtk.TreeViewColumn(None, renderer_text)
        label_column.add_attribute(renderer_text, "text", 3)
        label_column.set_property("min-width", 80)
        treeview.append_column(label_column)

        scrolledwindow = Gtk.ScrolledWindow(
            hexpand=True,
            vexpand=True,
            child=treeview,
            visible=True
        )
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        scrolledwindow.show_all()
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        self.continue_button.show()
        self.continue_button.set_sensitive(True)
        if self.continue_handler:
            self.continue_button.disconnect(self.continue_handler)
        self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_treestore)

    def on_extra_toggled(self, _widget, path, model):
        toggled_row = model[path]
        toggled_row_iter = model.get_iter(path)

        toggled_row[0] = not toggled_row[0]
        toggled_row[1] = False

        if model.iter_has_child(toggled_row_iter):
            extra_iter = model.iter_children(toggled_row_iter)
            while extra_iter:
                extra_row = model[extra_iter]
                extra_row[0] = toggled_row[0]
                extra_iter = model.iter_next(extra_iter)
        else:
            for heading_row in model:
                all_extras_active = True
                any_extras_active = False
                extra_iter = model.iter_children(heading_row.iter)
                while extra_iter:
                    extra_row = model[extra_iter]
                    if extra_row[0]:
                        any_extras_active = True
                    else:
                        all_extras_active = False
                    extra_iter = model.iter_next(extra_iter)

                heading_row[0] = all_extras_active
                heading_row[1] = any_extras_active

    def on_extras_confirmed(self, _button, extra_store):
        """Resume install when user has selected extras to download"""
        selected_extras = []

        def save_extra(store, path, iter_):
            selected, _inconsistent, id_, _label = store[iter_]
            if selected and id_:
                selected_extras.append(id_)
        extra_store.foreach(save_extra)

        self.interpreter.extras = selected_extras
        GLib.idle_add(self.on_runners_ready)

    def on_files_ready(self, _widget, files_ready):
        """Toggle state of continue button based on ready state"""
        self.continue_button.set_sensitive(files_ready)

    def on_files_confirmed(self, _button, file_box):
        """Call this when the user confirms the install files
        This will start the downloads.
        """
        self.set_status("")
        self.continue_button.set_sensitive(False)
        try:
            file_box.start_all()
            self.continue_button.disconnect(self.continue_handler)
        except PermissionError as ex:
            self.continue_button.set_sensitive(True)
            raise ScriptingError(_("Unable to get files: %s") % ex) from ex

    def on_files_available(self, widget):
        """All files are available, continue the install"""
        logger.info("All files are available, continuing install")
        self._cancel_files_func = None
        self.continue_button.hide()
        self.interpreter.game_files = widget.get_game_files()
        self.clean_widgets()
        self.interpreter.launch_installer_commands()

    def on_install_finished(self, game_id):
        self.clean_widgets()

        if self.config.get("create_desktop_shortcut"):
            self.create_shortcut(desktop=True)
        if self.config.get("create_menu_shortcut"):
            self.create_shortcut()

        # Save game to trigger a game-updated signal,
        # but take care not to create a blank game
        if game_id:
            game = Game(game_id)
            game.save()

        self.install_in_progress = False

        self.widget_box.show()

        self.eject_button.hide()
        self.cancel_button.hide()
        self.continue_button.hide()
        self.install_button.hide()
        if game and game.id:
            self.play_button.show()

        self.close_button.grab_focus()
        self.close_button.show()
        if not self.is_active():
            self.set_urgency_hint(True)  # Blink in taskbar
            self.connect("focus-in-event", self.on_window_focus)

    def on_window_focus(self, _widget, *_args):
        """Remove urgency hint (flashing indicator) when window receives focus"""
        self.set_urgency_hint(False)

    def on_install_error(self, message):
        self.clean_widgets()
        self.set_status(message)
        self.cancel_button.grab_focus()

    def launch_game(self, widget, _data=None):
        """Launch a game after it's been installed."""
        widget.set_sensitive(False)
        self.on_destroy(widget)
        game = Game(self.interpreter.installer.game_id)
        if game.id:
            game.emit("game-launch")
        else:
            logger.error("Game has no ID, launch button should not be drawn")

    def on_destroy(self, _widget, _data=None):
        """destroy event handler"""
        if self.install_in_progress:
            if self.confirm_cancel():
                return True
        else:
            if self.interpreter:
                self.interpreter.cleanup()
            self.destroy()

    def on_create_desktop_shortcut_clicked(self, _widget):
        self.config["create_desktop_shortcut"] = True

    def on_create_menu_shortcut_clicked(self, _widget):
        self.config["create_menu_shortcut"] = True

    def create_shortcut(self, desktop=False):
        """Create desktop or global menu shortcuts."""
        game_slug = self.interpreter.installer.game_slug
        game_id = self.interpreter.installer.game_id
        game_name = self.interpreter.installer.game_name

        if desktop:
            xdgshortcuts.create_launcher(game_slug, game_id, game_name, desktop=True)
        else:
            xdgshortcuts.create_launcher(game_slug, game_id, game_name, menu=True)

    def confirm_cancel(self, _widget=None):
        """Ask a confirmation before cancelling the install"""
        remove_checkbox = Gtk.CheckButton.new_with_label(_("Remove game files"))
        if self.interpreter and self.interpreter.target_path:
            remove_checkbox.set_active(self.interpreter.game_dir_created)
            remove_checkbox.show()
        confirm_cancel_dialog = QuestionDialog(
            {
                "question": _("Are you sure you want to cancel the installation?"),
                "title": _("Cancel installation?"),
                "widgets": [remove_checkbox]
            }
        )
        if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
            logger.debug("User aborted installation cancellation")
            return True
        if self._cancel_files_func:
            self._cancel_files_func()
        if self.interpreter:
            self.interpreter.revert(remove_game_dir=remove_checkbox.get_active())
            self.interpreter.cleanup()  # still remove temporary downloads in any case
        self.destroy()

    def on_source_clicked(self, _button):
        InstallerSourceDialog(
            self.interpreter.installer.script_pretty,
            self.interpreter.installer.game_name,
            self
        )

    def clean_widgets(self):
        """Cleanup before displaying the next stage."""
        for child_widget in self.widget_box.get_children():
            child_widget.destroy()

    def set_status(self, text):
        """Display a short status text."""
        self.status_label.set_text(text)

    def set_message(self, message):
        """Display a message."""
        label = InstallerLabel()
        label.set_markup("<b>%s</b>" % add_url_tags(message))
        label.show()
        self.widget_box.pack_start(label, False, False, 18)

    def add_spinner(self):
        """Show a spinner in the middle of the view"""
        self.clean_widgets()
        spinner = Gtk.Spinner()
        self.widget_box.pack_start(spinner, False, False, 18)
        spinner.show()
        spinner.start()

    def attach_logger(self, command):
        """Creates a TextBuffer and attach it to a command"""
        self.log_buffer = Gtk.TextBuffer()
        command.set_log_buffer(self.log_buffer)
        self.log_textview = LogTextView(self.log_buffer)
        scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.log_textview)
        scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
        self.widget_box.pack_end(scrolledwindow, True, True, 10)
        scrolledwindow.show()
        self.log_textview.show()
__init__(self, installers, service=None, appid=None, application=None, is_update=False) special
Source code in lutris/gui/installerwindow.py
def __init__(
    self,
    installers,
    service=None,
    appid=None,
    application=None,
    is_update=False
):
    super().__init__(application=application)
    self.set_default_size(540, 320)
    self.installers = installers
    self.config = {}
    self.service = service
    self.appid = appid
    self.install_in_progress = False
    self.interpreter = None
    self.is_update = is_update
    self.log_buffer = None
    self.log_textview = None

    self._cancel_files_func = None

    self.title_label = InstallerLabel()
    self.title_label.set_selectable(False)
    self.vbox.add(self.title_label)

    self.status_label = InstallerLabel()
    self.status_label.set_max_width_chars(80)
    self.status_label.set_property("wrap", True)
    self.status_label.set_selectable(True)
    self.vbox.add(self.status_label)

    self.widget_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    self.vbox.pack_start(self.widget_box, True, True, 0)

    self.vbox.add(Gtk.HSeparator())

    button_box = Gtk.Box()
    self.cache_button = Gtk.Button(_("Cache"))
    self.cache_button.connect("clicked", self.on_cache_clicked)
    button_box.add(self.cache_button)

    self.action_buttons = Gtk.Box(spacing=6)
    action_buttons_alignment = Gtk.Alignment.new(1, 0, 0, 0)
    action_buttons_alignment.add(self.action_buttons)
    button_box.pack_end(action_buttons_alignment, True, True, 0)
    self.vbox.pack_start(button_box, False, True, 0)

    self.cancel_button = self.add_button(
        _("C_ancel"), self.confirm_cancel, tooltip=_("Abort and revert the installation")
    )
    self.eject_button = self.add_button(_("_Eject"), self.on_eject_clicked)
    self.source_button = self.add_button(_("_View source"), self.on_source_clicked)
    self.install_button = self.add_button(_("_Install"), self.on_install_clicked)
    self.continue_button = self.add_button(_("_Continue"))
    self.play_button = self.add_button(_("_Launch"), self.launch_game)
    self.close_button = self.add_button(_("_Close"), self.on_destroy)

    self.continue_handler = None

    self.clean_widgets()
    self.show_all()
    self.close_button.hide()
    self.play_button.hide()
    self.install_button.hide()
    self.source_button.hide()
    self.eject_button.hide()
    self.continue_button.hide()
    self.install_in_progress = True
    self.widget_box.show()
    self.title_label.show()
    self.choose_installer()

    self.present()
add_button(self, label, handler=None, tooltip=None)

Add a button to the action buttons box

Source code in lutris/gui/installerwindow.py
def add_button(self, label, handler=None, tooltip=None):
    """Add a button to the action buttons box"""
    button = Gtk.Button.new_with_mnemonic(label)
    if tooltip:
        button.set_tooltip_text(tooltip)
    if handler:
        button.connect("clicked", handler)
    self.action_buttons.add(button)
    return button
add_spinner(self)

Show a spinner in the middle of the view

Source code in lutris/gui/installerwindow.py
def add_spinner(self):
    """Show a spinner in the middle of the view"""
    self.clean_widgets()
    spinner = Gtk.Spinner()
    self.widget_box.pack_start(spinner, False, False, 18)
    spinner.show()
    spinner.start()
ask_for_disc(self, message, callback, requires)

Ask the user to do insert a CD-ROM.

Source code in lutris/gui/installerwindow.py
def ask_for_disc(self, message, callback, requires):
    """Ask the user to do insert a CD-ROM."""
    self.clean_widgets()
    label = InstallerLabel(message)
    label.show()
    self.widget_box.add(label)

    buttons_box = Gtk.Box()
    buttons_box.show()
    buttons_box.set_margin_top(40)
    buttons_box.set_margin_bottom(40)
    self.widget_box.add(buttons_box)

    autodetect_button = Gtk.Button(label=_("Autodetect"))
    autodetect_button.connect("clicked", callback, requires)
    autodetect_button.grab_focus()
    autodetect_button.show()
    buttons_box.pack_start(autodetect_button, True, True, 40)

    browse_button = Gtk.Button(label=_("Browse…"))
    callback_data = {"callback": callback, "requires": requires}
    browse_button.connect("clicked", self.on_browse_clicked, callback_data)
    browse_button.show()
    buttons_box.pack_start(browse_button, True, True, 40)
attach_logger(self, command)

Creates a TextBuffer and attach it to a command

Source code in lutris/gui/installerwindow.py
def attach_logger(self, command):
    """Creates a TextBuffer and attach it to a command"""
    self.log_buffer = Gtk.TextBuffer()
    command.set_log_buffer(self.log_buffer)
    self.log_textview = LogTextView(self.log_buffer)
    scrolledwindow = Gtk.ScrolledWindow(hexpand=True, vexpand=True, child=self.log_textview)
    scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
    self.widget_box.pack_end(scrolledwindow, True, True, 10)
    scrolledwindow.show()
    self.log_textview.show()
choose_installer(self)

Stage where we choose an install script.

Source code in lutris/gui/installerwindow.py
def choose_installer(self):
    """Stage where we choose an install script."""
    self.validate_scripts()
    base_script = self.installers[0]
    self.title_label.set_markup(_("<b>Install %s</b>") % gtk_safe(base_script["name"]))
    installer_picker = InstallerPicker(self.installers)
    installer_picker.connect("installer-selected", self.on_installer_selected)
    scrolledwindow = Gtk.ScrolledWindow(
        hexpand=True,
        vexpand=True,
        child=installer_picker,
        visible=True
    )
    scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
    self.widget_box.pack_end(scrolledwindow, True, True, 10)
clean_widgets(self)

Cleanup before displaying the next stage.

Source code in lutris/gui/installerwindow.py
def clean_widgets(self):
    """Cleanup before displaying the next stage."""
    for child_widget in self.widget_box.get_children():
        child_widget.destroy()
confirm_cancel(self, _widget=None)

Ask a confirmation before cancelling the install

Source code in lutris/gui/installerwindow.py
def confirm_cancel(self, _widget=None):
    """Ask a confirmation before cancelling the install"""
    remove_checkbox = Gtk.CheckButton.new_with_label(_("Remove game files"))
    if self.interpreter and self.interpreter.target_path:
        remove_checkbox.set_active(self.interpreter.game_dir_created)
        remove_checkbox.show()
    confirm_cancel_dialog = QuestionDialog(
        {
            "question": _("Are you sure you want to cancel the installation?"),
            "title": _("Cancel installation?"),
            "widgets": [remove_checkbox]
        }
    )
    if confirm_cancel_dialog.result != Gtk.ResponseType.YES:
        logger.debug("User aborted installation cancellation")
        return True
    if self._cancel_files_func:
        self._cancel_files_func()
    if self.interpreter:
        self.interpreter.revert(remove_game_dir=remove_checkbox.get_active())
        self.interpreter.cleanup()  # still remove temporary downloads in any case
    self.destroy()
create_shortcut(self, desktop=False)

Create desktop or global menu shortcuts.

Source code in lutris/gui/installerwindow.py
def create_shortcut(self, desktop=False):
    """Create desktop or global menu shortcuts."""
    game_slug = self.interpreter.installer.game_slug
    game_id = self.interpreter.installer.game_id
    game_name = self.interpreter.installer.game_name

    if desktop:
        xdgshortcuts.create_launcher(game_slug, game_id, game_name, desktop=True)
    else:
        xdgshortcuts.create_launcher(game_slug, game_id, game_name, menu=True)
get_extra_label(self, extra)

Return a label for the extras picker

Source code in lutris/gui/installerwindow.py
def get_extra_label(self, extra):
    """Return a label for the extras picker"""
    label = extra["name"]
    _infos = []
    if extra.get("total_size"):
        _infos.append(human_size(extra["total_size"]))
    if extra.get("type"):
        _infos.append(extra["type"])
    if _infos:
        label += " (%s)" % ", ".join(_infos)
    return label
input_menu(self, alias, options, preselect, has_entry, callback)

Display an input request as a dropdown menu with options.

Source code in lutris/gui/installerwindow.py
def input_menu(self, alias, options, preselect, has_entry, callback):
    """Display an input request as a dropdown menu with options."""
    self.clean_widgets()

    model = Gtk.ListStore(str, str)
    for option in options:
        key, label = option.popitem()
        model.append([key, label])
    combobox = Gtk.ComboBox.new_with_model(model)
    renderer_text = Gtk.CellRendererText()
    combobox.pack_start(renderer_text, True)
    combobox.add_attribute(renderer_text, "text", 1)
    combobox.set_id_column(0)
    combobox.set_active_id(preselect)
    combobox.set_halign(Gtk.Align.CENTER)
    self.widget_box.pack_start(combobox, True, False, 100)

    combobox.connect("changed", self.on_input_menu_changed)
    combobox.show()
    if self.continue_handler:
        self.continue_button.disconnect(self.continue_handler)
    self.continue_handler = self.continue_button.connect("clicked", callback, alias, combobox)
    self.continue_button.grab_focus()
    self.continue_button.show()
    self.on_input_menu_changed(combobox)
launch_game(self, widget, _data=None)

Launch a game after it's been installed.

Source code in lutris/gui/installerwindow.py
def launch_game(self, widget, _data=None):
    """Launch a game after it's been installed."""
    widget.set_sensitive(False)
    self.on_destroy(widget)
    game = Game(self.interpreter.installer.game_id)
    if game.id:
        game.emit("game-launch")
    else:
        logger.error("Game has no ID, launch button should not be drawn")
on_browse_clicked(self, widget, callback_data)
Source code in lutris/gui/installerwindow.py
def on_browse_clicked(self, widget, callback_data):
    dialog = DirectoryDialog(_("Select the folder where the disc is mounted"), parent=self)
    folder = dialog.folder
    callback = callback_data["callback"]
    requires = callback_data["requires"]
    callback(widget, requires, folder)
on_cache_clicked(self, _button)

Open the cache configuration dialog

Source code in lutris/gui/installerwindow.py
def on_cache_clicked(self, _button):
    """Open the cache configuration dialog"""
    CacheConfigurationDialog()
on_create_desktop_shortcut_clicked(self, _widget)
Source code in lutris/gui/installerwindow.py
def on_create_desktop_shortcut_clicked(self, _widget):
    self.config["create_desktop_shortcut"] = True
on_create_menu_shortcut_clicked(self, _widget)
Source code in lutris/gui/installerwindow.py
def on_create_menu_shortcut_clicked(self, _widget):
    self.config["create_menu_shortcut"] = True
on_destroy(self, _widget, _data=None)

destroy event handler

Source code in lutris/gui/installerwindow.py
def on_destroy(self, _widget, _data=None):
    """destroy event handler"""
    if self.install_in_progress:
        if self.confirm_cancel():
            return True
    else:
        if self.interpreter:
            self.interpreter.cleanup()
        self.destroy()
on_eject_clicked(self, _widget, data=None)
Source code in lutris/gui/installerwindow.py
def on_eject_clicked(self, _widget, data=None):
    self.interpreter.eject_wine_disc()
on_extra_toggled(self, _widget, path, model)
Source code in lutris/gui/installerwindow.py
def on_extra_toggled(self, _widget, path, model):
    toggled_row = model[path]
    toggled_row_iter = model.get_iter(path)

    toggled_row[0] = not toggled_row[0]
    toggled_row[1] = False

    if model.iter_has_child(toggled_row_iter):
        extra_iter = model.iter_children(toggled_row_iter)
        while extra_iter:
            extra_row = model[extra_iter]
            extra_row[0] = toggled_row[0]
            extra_iter = model.iter_next(extra_iter)
    else:
        for heading_row in model:
            all_extras_active = True
            any_extras_active = False
            extra_iter = model.iter_children(heading_row.iter)
            while extra_iter:
                extra_row = model[extra_iter]
                if extra_row[0]:
                    any_extras_active = True
                else:
                    all_extras_active = False
                extra_iter = model.iter_next(extra_iter)

            heading_row[0] = all_extras_active
            heading_row[1] = any_extras_active
on_extras_confirmed(self, _button, extra_store)

Resume install when user has selected extras to download

Source code in lutris/gui/installerwindow.py
def on_extras_confirmed(self, _button, extra_store):
    """Resume install when user has selected extras to download"""
    selected_extras = []

    def save_extra(store, path, iter_):
        selected, _inconsistent, id_, _label = store[iter_]
        if selected and id_:
            selected_extras.append(id_)
    extra_store.foreach(save_extra)

    self.interpreter.extras = selected_extras
    GLib.idle_add(self.on_runners_ready)
on_files_available(self, widget)

All files are available, continue the install

Source code in lutris/gui/installerwindow.py
def on_files_available(self, widget):
    """All files are available, continue the install"""
    logger.info("All files are available, continuing install")
    self._cancel_files_func = None
    self.continue_button.hide()
    self.interpreter.game_files = widget.get_game_files()
    self.clean_widgets()
    self.interpreter.launch_installer_commands()
on_files_confirmed(self, _button, file_box)

Call this when the user confirms the install files This will start the downloads.

Source code in lutris/gui/installerwindow.py
def on_files_confirmed(self, _button, file_box):
    """Call this when the user confirms the install files
    This will start the downloads.
    """
    self.set_status("")
    self.continue_button.set_sensitive(False)
    try:
        file_box.start_all()
        self.continue_button.disconnect(self.continue_handler)
    except PermissionError as ex:
        self.continue_button.set_sensitive(True)
        raise ScriptingError(_("Unable to get files: %s") % ex) from ex
on_files_ready(self, _widget, files_ready)

Toggle state of continue button based on ready state

Source code in lutris/gui/installerwindow.py
def on_files_ready(self, _widget, files_ready):
    """Toggle state of continue button based on ready state"""
    self.continue_button.set_sensitive(files_ready)
on_input_menu_changed(self, widget)

Enable continue button if a non-empty choice is selected

Source code in lutris/gui/installerwindow.py
def on_input_menu_changed(self, widget):
    """Enable continue button if a non-empty choice is selected"""
    self.continue_button.set_sensitive(bool(widget.get_active_id()))
on_install_clicked(self, button)

Let the interpreter take charge of the next stages.

Source code in lutris/gui/installerwindow.py
def on_install_clicked(self, button):
    """Let the interpreter take charge of the next stages."""
    button.hide()
    self.source_button.hide()
    self.interpreter.connect("runners-installed", self.on_runners_ready)
    GLib.idle_add(self.interpreter.launch_install)
on_install_error(self, message)
Source code in lutris/gui/installerwindow.py
def on_install_error(self, message):
    self.clean_widgets()
    self.set_status(message)
    self.cancel_button.grab_focus()
on_install_finished(self, game_id)
Source code in lutris/gui/installerwindow.py
def on_install_finished(self, game_id):
    self.clean_widgets()

    if self.config.get("create_desktop_shortcut"):
        self.create_shortcut(desktop=True)
    if self.config.get("create_menu_shortcut"):
        self.create_shortcut()

    # Save game to trigger a game-updated signal,
    # but take care not to create a blank game
    if game_id:
        game = Game(game_id)
        game.save()

    self.install_in_progress = False

    self.widget_box.show()

    self.eject_button.hide()
    self.cancel_button.hide()
    self.continue_button.hide()
    self.install_button.hide()
    if game and game.id:
        self.play_button.show()

    self.close_button.grab_focus()
    self.close_button.show()
    if not self.is_active():
        self.set_urgency_hint(True)  # Blink in taskbar
        self.connect("focus-in-event", self.on_window_focus)
on_installer_selected(self, _widget, installer_version)

Sets the script interpreter to the correct script then proceed to install folder selection.

If the installed game depends on another one and it's not installed, prompt the user to install it and quit this installer.

Source code in lutris/gui/installerwindow.py
def on_installer_selected(self, _widget, installer_version):
    """Sets the script interpreter to the correct script then proceed to
    install folder selection.

    If the installed game depends on another one and it's not installed,
    prompt the user to install it and quit this installer.
    """
    self.clean_widgets()
    try:
        script = None
        for _script in self.installers:
            if _script["version"] == installer_version:
                script = _script
        self.interpreter = interpreter.ScriptInterpreter(script, self)

    except MissingGameDependency as ex:
        dlg = QuestionDialog(
            {
                "question": _("This game requires %s. Do you want to install it?") % ex.slug,
                "title": _("Missing dependency"),
            }
        )
        if dlg.result == Gtk.ResponseType.YES:
            InstallerWindow(
                installers=self.installers,
                service=self.service,
                appid=self.appid,
                application=self.application,
            )
        self.destroy()
        return
    self.title_label.set_markup(_("<b>Installing {}</b>").format(gtk_safe(self.interpreter.installer.game_name)))
    self.select_install_folder()

    desktop_shortcut_button = Gtk.CheckButton(_("Create desktop shortcut"), visible=True)
    desktop_shortcut_button.connect("clicked", self.on_create_desktop_shortcut_clicked)
    self.widget_box.pack_start(desktop_shortcut_button, False, False, 5)

    menu_shortcut_button = Gtk.CheckButton(_("Create application menu shortcut"), visible=True)
    menu_shortcut_button.connect("clicked", self.on_create_menu_shortcut_clicked)
    self.widget_box.pack_start(menu_shortcut_button, False, False, 5)
on_runners_ready(self, _widget=None)

The runners are ready, proceed with file selection

Source code in lutris/gui/installerwindow.py
def on_runners_ready(self, _widget=None):
    """The runners are ready, proceed with file selection"""
    if self.interpreter.extras is None:
        extras = self.interpreter.get_extras()
        if extras:
            self.show_extras(extras)
            return
    try:
        patch_version = self.interpreter.installer.version if self.is_update else None
        self.interpreter.installer.prepare_game_files(patch_version)
    except UnavailableGame as ex:
        raise ScriptingError(str(ex)) from ex

    if not self.interpreter.installer.files:
        logger.debug("Installer doesn't require files")
        self.interpreter.launch_installer_commands()
        return
    self.show_installer_files_screen()
on_source_clicked(self, _button)
Source code in lutris/gui/installerwindow.py
def on_source_clicked(self, _button):
    InstallerSourceDialog(
        self.interpreter.installer.script_pretty,
        self.interpreter.installer.game_name,
        self
    )
on_target_changed(self, text_entry, _data=None)

Set the installation target for the game.

Source code in lutris/gui/installerwindow.py
def on_target_changed(self, text_entry, _data=None):
    """Set the installation target for the game."""
    self.interpreter.target_path = os.path.expanduser(text_entry.get_text())
on_window_focus(self, _widget, *_args)

Remove urgency hint (flashing indicator) when window receives focus

Source code in lutris/gui/installerwindow.py
def on_window_focus(self, _widget, *_args):
    """Remove urgency hint (flashing indicator) when window receives focus"""
    self.set_urgency_hint(False)
select_install_folder(self)

Stage where we select the install directory.

Source code in lutris/gui/installerwindow.py
def select_install_folder(self):
    """Stage where we select the install directory."""
    if not self.interpreter.installer.creates_game_folder:
        self.on_install_clicked(self.install_button)
        return
    self.set_message(_("Select installation directory"))
    default_path = self.interpreter.get_default_target()
    self.set_install_destination(default_path)
    if self.continue_handler:
        self.continue_button.disconnect(self.continue_handler)
    self.continue_button.hide()
    self.source_button.show()
    self.install_button.grab_focus()
    self.install_button.show()
    # self.manual_button.hide()
set_install_destination(self, default_path=None)

Display the destination chooser.

Source code in lutris/gui/installerwindow.py
def set_install_destination(self, default_path=None):
    """Display the destination chooser."""
    self.install_button.set_visible(False)
    self.continue_button.show()
    self.continue_button.set_sensitive(False)
    location_entry = FileChooserEntry(
        "Select folder",
        Gtk.FileChooserAction.SELECT_FOLDER,
        path=default_path,
        warn_if_non_empty=True,
        warn_if_ntfs=True
    )
    location_entry.entry.connect("changed", self.on_target_changed)
    self.widget_box.pack_start(location_entry, False, False, 0)
set_message(self, message)

Display a message.

Source code in lutris/gui/installerwindow.py
def set_message(self, message):
    """Display a message."""
    label = InstallerLabel()
    label.set_markup("<b>%s</b>" % add_url_tags(message))
    label.show()
    self.widget_box.pack_start(label, False, False, 18)
set_status(self, text)

Display a short status text.

Source code in lutris/gui/installerwindow.py
def set_status(self, text):
    """Display a short status text."""
    self.status_label.set_text(text)
show_extras(self, all_extras)

Show installer screen with the extras picker

Source code in lutris/gui/installerwindow.py
def show_extras(self, all_extras):
    """Show installer screen with the extras picker"""
    self.clean_widgets()
    extra_treestore = Gtk.TreeStore(
        bool,  # is selected?
        bool,  # is inconsistent?
        str,   # id
        str,   # label
    )
    for extra_source, extras in all_extras.items():
        parent = extra_treestore.append(None, (None, None, None, extra_source))
        for extra in extras:
            extra_treestore.append(parent, (False, False, extra["id"], self.get_extra_label(extra)))

    treeview = Gtk.TreeView(extra_treestore)
    treeview.set_headers_visible(False)
    treeview.expand_all()
    renderer_toggle = Gtk.CellRendererToggle()
    renderer_toggle.connect("toggled", self.on_extra_toggled, extra_treestore)
    renderer_text = Gtk.CellRendererText()

    installed_column = Gtk.TreeViewColumn(None, renderer_toggle, active=0, inconsistent=1)
    treeview.append_column(installed_column)

    label_column = Gtk.TreeViewColumn(None, renderer_text)
    label_column.add_attribute(renderer_text, "text", 3)
    label_column.set_property("min-width", 80)
    treeview.append_column(label_column)

    scrolledwindow = Gtk.ScrolledWindow(
        hexpand=True,
        vexpand=True,
        child=treeview,
        visible=True
    )
    scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
    scrolledwindow.show_all()
    self.widget_box.pack_end(scrolledwindow, True, True, 10)
    self.continue_button.show()
    self.continue_button.set_sensitive(True)
    if self.continue_handler:
        self.continue_button.disconnect(self.continue_handler)
    self.continue_handler = self.continue_button.connect("clicked", self.on_extras_confirmed, extra_treestore)
show_installer_files_screen(self)

Show installer screen with the file picker / downloader

Source code in lutris/gui/installerwindow.py
def show_installer_files_screen(self):
    """Show installer screen with the file picker / downloader"""
    self.clean_widgets()
    self.set_status(_("Please review the files needed for the installation then click 'Continue'"))
    installer_files_box = InstallerFilesBox(self.interpreter.installer, self)
    installer_files_box.connect("files-available", self.on_files_available)
    installer_files_box.connect("files-ready", self.on_files_ready)
    self._cancel_files_func = installer_files_box.stop_all
    scrolledwindow = Gtk.ScrolledWindow(
        hexpand=True,
        vexpand=True,
        child=installer_files_box,
        visible=True
    )
    scrolledwindow.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
    self.widget_box.pack_end(scrolledwindow, True, True, 10)

    self.continue_button.show()
    self.continue_button.set_sensitive(installer_files_box.is_ready)
    if self.continue_handler:
        self.continue_button.disconnect(self.continue_handler)
    self.continue_handler = self.continue_button.connect(
        "clicked", self.on_files_confirmed, installer_files_box
    )
validate_scripts(self)

Auto-fixes some script aspects and checks for mandatory fields

Source code in lutris/gui/installerwindow.py
def validate_scripts(self):
    """Auto-fixes some script aspects and checks for mandatory fields"""
    if not self.installers:
        raise ScriptingError("No installer available")
    for script in self.installers:
        for item in ["description", "notes"]:
            script[item] = script.get(item) or ""
        for item in ["name", "runner", "version"]:
            if item not in script:
                logger.error("Invalid script: %s", script)
                raise ScriptingError(_('Missing field "%s" in install script') % item)

lutriswindow

Main window for the Lutris interface.

LutrisWindow (ApplicationWindow)

Handler class for main window signals.

Source code in lutris/gui/lutriswindow.py
@GtkTemplate(ui=os.path.join(datapath.get(), "ui", "lutris-window.ui"))
class LutrisWindow(Gtk.ApplicationWindow):  # pylint: disable=too-many-public-methods
    """Handler class for main window signals."""

    default_view_type = "grid"
    default_width = 800
    default_height = 600

    __gtype_name__ = "LutrisWindow"
    __gsignals__ = {
        "view-updated": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    games_scrollwindow = GtkTemplate.Child()
    sidebar_revealer = GtkTemplate.Child()
    sidebar_scrolled = GtkTemplate.Child()
    game_revealer = GtkTemplate.Child()
    search_entry = GtkTemplate.Child()
    zoom_adjustment = GtkTemplate.Child()
    blank_overlay = GtkTemplate.Child()
    viewtype_icon = GtkTemplate.Child()

    def __init__(self, application, **kwargs):
        width = int(settings.read_setting("width") or self.default_width)
        height = int(settings.read_setting("height") or self.default_height)
        super().__init__(
            default_width=width,
            default_height=height,
            window_position=Gtk.WindowPosition.NONE,
            name="lutris",
            icon_name="lutris",
            application=application,
            **kwargs
        )
        update_desktop_icons()
        load_icon_theme()
        self.application = application
        self.window_x = settings.read_setting("window_x")
        self.window_y = settings.read_setting("window_y")
        if self.window_x and self.window_y:
            self.move(int(self.window_x), int(self.window_y))
        self.threads_stoppers = []
        self.window_size = (width, height)
        self.maximized = settings.read_setting("maximized") == "True"
        self.service = None
        self.game_actions = GameActions(application=application, window=self)
        self.search_timer_id = None
        self.selected_category = settings.read_setting("selected_category", default="runner:all")
        self.filters = self.load_filters()
        self.set_service(self.filters.get("service"))
        self.icon_type = self.load_icon_type()
        self.game_store = GameStore(self.service, self.service_media)
        self.view = Gtk.Box()

        self.connect("delete-event", self.on_window_delete)
        self.connect("configure-event", self.on_window_configure)
        self.connect("realize", self.on_load)
        if self.maximized:
            self.maximize()

        self.init_template()
        self._init_actions()

        self.set_viewtype_icon(self.view_type)

        lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
        lutris_icon.set_margin_right(3)

        self.sidebar = LutrisSidebar(self.application, selected=self.selected_category)
        self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
        # "realize" is order sensitive- must connect after sidebar itself connects the same signal
        self.sidebar.connect("realize", self.on_sidebar_realize)
        self.sidebar_scrolled.add(self.sidebar)

        # This must wait until the selected-rows-changed signal is connected
        self.sidebar.initialize_rows()

        self.sidebar_revealer.set_reveal_child(self.side_panel_visible)
        self.sidebar_revealer.set_transition_duration(300)

        self.game_bar = None
        self.revealer_box = Gtk.HBox(visible=True)
        self.game_revealer.add(self.revealer_box)

        self.connect("view-updated", self.update_store)
        GObject.add_emission_hook(BaseService, "service-login", self.on_service_login)
        GObject.add_emission_hook(BaseService, "service-logout", self.on_service_logout)
        GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
        GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
        GObject.add_emission_hook(Game, "game-stopped", self.on_game_stopped)
        GObject.add_emission_hook(Game, "game-removed", self.on_game_collection_changed)

    def _init_actions(self):
        Action = namedtuple("Action", ("callback", "type", "enabled", "default", "accel"))
        Action.__new__.__defaults__ = (None, None, True, None, None)

        actions = {
            "add-game": Action(self.on_add_game_button_clicked),
            "preferences": Action(self.on_preferences_activate),
            "about": Action(self.on_about_clicked),
            "show-installed-only": Action(  # delete?
                self.on_show_installed_state_change,
                type="b",
                default=self.filter_installed,
                accel="<Primary>h",
            ),
            "toggle-viewtype": Action(self.on_toggle_viewtype),
            "icon-type": Action(self.on_icontype_state_change, type="s", default=self.icon_type),
            "view-sorting": Action(self.on_view_sorting_state_change, type="s", default=self.view_sorting),
            "view-sorting-ascending": Action(
                self.on_view_sorting_direction_change,
                type="b",
                default=self.view_sorting_ascending,
            ),
            "show-side-panel": Action(
                self.on_side_panel_state_change,
                type="b",
                default=self.side_panel_visible,
                accel="F9",
            ),
            "show-hidden-games": Action(
                self.hidden_state_change,
                type="b",
                default=self.show_hidden_games,
            ),
            "open-forums": Action(lambda *x: open_uri("https://forums.lutris.net/")),
            "open-discord": Action(lambda *x: open_uri("https://discord.gg/Pnt5CuY")),
            "donate": Action(lambda *x: open_uri("https://lutris.net/donate")),
        }

        self.actions = {}
        app = self.props.application
        for name, value in actions.items():
            if not value.type:
                action = Gio.SimpleAction.new(name)
                action.connect("activate", value.callback)
            else:
                default_value = None
                param_type = None
                if value.default is not None:
                    default_value = GLib.Variant(value.type, value.default)
                if value.type != "b":
                    param_type = default_value.get_type()
                action = Gio.SimpleAction.new_stateful(name, param_type, default_value)
                action.connect("change-state", value.callback)
            self.actions[name] = action
            if value.enabled is False:
                action.props.enabled = False
            self.add_action(action)
            if value.accel:
                app.add_accelerator(value.accel, "win." + name)

    @property
    def service_media(self):
        return self.get_service_media(self.load_icon_type())

    def on_load(self, widget, data=None):
        """Finish initializing the view"""
        self._bind_zoom_adjustment()
        self.view.grab_focus()
        self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())

    def on_sidebar_realize(self, widget, data=None):
        """Grab the initial focus after the sidebar is initialized - so the view is ready."""
        self.view.grab_focus()

    def load_filters(self):
        """Load the initial filters when creating the view"""
        category, value = self.selected_category.split(":")
        filters = {
            category: value
        }  # Type of filter corresponding to the selected sidebar element
        filters["hidden"] = settings.read_setting("show_hidden_games").lower() == "true"
        filters["installed"] = settings.read_setting("filter_installed").lower() == "true"
        return filters

    def hidden_state_change(self, action, value):
        """Hides or shows the hidden games"""
        action.set_state(value)
        settings.write_setting("show_hidden_games", str(value).lower(), section="lutris")
        self.filters["hidden"] = value
        self.emit("view-updated")

    @property
    def current_view_type(self):
        """Returns which kind of view is currently presented (grid or list)"""
        return settings.read_setting("view_type") or "grid"

    @property
    def filter_installed(self):
        return settings.read_setting("filter_installed").lower() == "true"

    @property
    def side_panel_visible(self):
        return settings.read_setting("side_panel_visible").lower() != "false"

    @property
    def show_tray_icon(self):
        """Setting to hide or show status icon"""
        return settings.read_setting("show_tray_icon", default="false").lower() == "true"

    @property
    def view_sorting(self):
        value = settings.read_setting("view_sorting") or "name"
        if value.endswith("_text"):
            value = value[:-5]
        return value

    @property
    def view_sorting_ascending(self):
        return settings.read_setting("view_sorting_ascending").lower() != "false"

    @property
    def show_hidden_games(self):
        return settings.read_setting("show_hidden_games").lower() == "true"

    @property
    def sort_params(self):
        _sort_params = [("installed", "COLLATE NOCASE DESC")]
        _sort_params.append((
            self.view_sorting,
            "COLLATE NOCASE ASC"
            if self.view_sorting_ascending
            else "COLLATE NOCASE DESC"
        ))
        return _sort_params

    def get_running_games(self):
        """Return a list of currently running games"""
        return games_db.get_games_by_ids([game.id for game in self.application.running_games])

    def get_recent_games(self):
        """Return a list of currently running games"""
        searches, _filters, excludes = self.get_sql_filters()
        games = games_db.get_games(searches=searches, filters={'installed': '1'}, excludes=excludes)
        return sorted(
            games,
            key=lambda game: max(game["installed_at"] or 0, game["lastplayed"] or 0),
            reverse=True
        )

    def game_matches(self, game):
        if self.filters.get("installed"):
            if game["appid"] not in games_db.get_service_games(self.service.id):
                return False
        if not self.filters.get("text"):
            return True
        return self.filters["text"] in game["name"].lower()

    def set_service(self, service_name):
        if self.service and self.service.id == service_name:
            return self.service
        if not service_name:
            self.service = None
            return
        try:
            self.service = services.SERVICES[service_name]()
        except KeyError:
            logger.error("Non existent service '%s'", service_name)
            self.service = None
        return self.service

    @staticmethod
    def combine_games(service_game, lutris_game):
        """Inject lutris game information into a service game"""
        if lutris_game and service_game["appid"] == lutris_game["service_id"]:
            for field in ("platform", "runner", "year", "installed_at", "lastplayed", "playtime", "installed"):
                service_game[field] = lutris_game[field]
        return service_game

    def get_service_games(self, service_name):
        """Switch the current service to service_name and return games if available"""
        service_games = ServiceGameCollection.get_for_service(service_name)
        if service_name == "lutris":
            lutris_games = {g["slug"]: g for g in games_db.get_games()}
        else:
            lutris_games = {g["service_id"]: g for g in games_db.get_games(filters={"service": self.service.id})}

        def get_sort_value(game):
            sort_defaults = {
                "name": "",
                "year": 0,
                "lastplayed": 0.0,
                "installed_at": 0.0,
                "playtime": 0.0,
            }
            view_sorting = self.view_sorting
            lutris_game = lutris_games.get(game["appid"])
            if not lutris_game:
                return sort_defaults.get(view_sorting, "")
            value = lutris_game.get(view_sorting)
            if value:
                return value
            # Users may have obsolete view_sorting settings, so
            # we must tolerate them. We treat them all as blank.
            return sort_defaults.get(view_sorting, "")

        return [
            self.combine_games(game, lutris_games.get(game["appid"])) for game in sorted(
                service_games,
                key=get_sort_value,
                reverse=not self.view_sorting_ascending
            ) if self.game_matches(game)
        ]

    def get_games_from_filters(self):
        service_name = self.filters.get("service")
        if service_name in services.SERVICES:
            if self.service.online and not self.service.is_authenticated():
                self.show_label(_("Connect your %s account to access your games") % self.service.name)
                return []
            return self.get_service_games(service_name)
        dynamic_categories = {
            "recent": self.get_recent_games,
            "running": self.get_running_games,
        }
        if self.filters.get("dynamic_category") in dynamic_categories:
            return dynamic_categories[self.filters["dynamic_category"]]()
        if self.filters.get("category") and self.filters["category"] != "all":
            game_ids = categories_db.get_game_ids_for_category(self.filters["category"])
        else:
            game_ids = None
        searches, filters, excludes = self.get_sql_filters()
        games = games_db.get_games(
            searches=searches,
            filters=filters,
            excludes=excludes,
            sorts=self.sort_params
        )
        if game_ids is not None:
            return [game for game in games if game["id"] in game_ids]
        return games

    def get_sql_filters(self):
        """Return the current filters for the view"""
        sql_filters = {}
        sql_excludes = {}
        if self.filters.get("runner"):
            sql_filters["runner"] = self.filters["runner"]
        if self.filters.get("platform"):
            sql_filters["platform"] = self.filters["platform"]
        if self.filters.get("installed"):
            sql_filters["installed"] = "1"
        if self.filters.get("text"):
            searches = {"name": self.filters["text"]}
        else:
            searches = None
        if not self.filters.get("hidden"):
            sql_excludes["hidden"] = 1
        return searches, sql_filters, sql_excludes

    def get_service_media(self, icon_type):
        """Return the ServiceMedia class used for this view"""
        service = self.service if self.service else LutrisService
        medias = service.medias
        if icon_type in medias:
            return medias[icon_type]()
        return medias[service.default_format]()

    def update_revealer(self, game=None):
        if game:
            if self.game_bar:
                self.game_bar.destroy()
            self.game_bar = GameBar(game, self.game_actions, self.application)
            self.revealer_box.pack_start(self.game_bar, True, True, 0)
        elif self.game_bar:
            # The game bar can't be destroyed here because the game gets unselected on Wayland
            # whenever the game bar is interacted with. Instead, we keep the current game bar open
            # when the game gets unselected, which is somewhat closer to what the intended behavior
            # should be anyway. Might require closing the game bar manually in some cases.
            pass
            # self.game_bar.destroy()
        if self.revealer_box.get_children():
            self.game_revealer.set_reveal_child(True)
        else:
            self.game_revealer.set_reveal_child(False)

    def show_empty_label(self):
        """Display a label when the view is empty"""
        if self.filters.get("text"):
            self.show_label(_("No games matching '%s' found ") % self.filters["text"])
        else:
            if self.filters.get("category") == "favorite":
                self.show_label(_("Add games to your favorites to see them here."))
            elif self.filters.get("installed"):
                self.show_label(_("No installed games found. Press Ctrl+H so show all games."))
            else:
                self.show_splash()
                # self.show_label(_("No games found"))

    def update_store(self, *_args, **_kwargs):
        self.game_store.store.clear()
        for child in self.blank_overlay.get_children():
            child.destroy()
        games = self.get_games_from_filters()
        logger.debug("Showing %d games", len(games))
        self.view.service = self.service.id if self.service else None
        GLib.idle_add(self.update_revealer)
        for game in games:
            self.game_store.add_game(game)
        if not games:
            self.show_empty_label()
        self.search_timer_id = None
        return False

    def _bind_zoom_adjustment(self):
        """Bind the zoom slider to the supported banner sizes"""
        service = self.service if self.service else LutrisService
        media_services = list(service.medias.keys())
        self.load_icon_type()
        self.zoom_adjustment.set_lower(0)
        self.zoom_adjustment.set_upper(len(media_services) - 1)
        if self.icon_type in media_services:
            value = media_services.index(self.icon_type)
        else:
            value = 0
        self.zoom_adjustment.props.value = value
        self.zoom_adjustment.connect("value-changed", self.on_zoom_changed)

    def on_zoom_changed(self, adjustment):
        """Handler for zoom modification"""
        media_index = round(adjustment.props.value)
        adjustment.props.value = media_index
        service = self.service if self.service else LutrisService
        media_services = list(service.medias.keys())
        if len(media_services) <= media_index:
            media_index = media_services.index(service.default_format)
        icon_type = media_services[media_index]
        if icon_type != self.icon_type:
            self.save_icon_type(icon_type)
            self.show_spinner()

    def show_overlay(self, widget, halign=Gtk.Align.FILL, valign=Gtk.Align.FILL):
        """Display a widget in the blank overlay"""
        for child in self.blank_overlay.get_children():
            child.destroy()
        self.blank_overlay.set_halign(halign)
        self.blank_overlay.set_valign(valign)
        self.blank_overlay.add(widget)
        self.blank_overlay.props.visible = True

    def show_label(self, message):
        """Display a label in the middle of the UI"""
        self.show_overlay(Gtk.Label(message, visible=True))

    def show_splash(self):
        image = Gtk.Image(visible=True)
        image.set_from_file(os.path.join(datapath.get(), "media/splash.svg"))
        self.show_overlay(image, Gtk.Align.START, Gtk.Align.START)

    def show_spinner(self):
        spinner = Gtk.Spinner(visible=True)
        spinner.start()
        for child in self.blank_overlay.get_children():
            child.destroy()
        self.blank_overlay.add(spinner)
        self.blank_overlay.props.visible = True

    def hide_overlay(self):
        self.blank_overlay.props.visible = False
        for child in self.blank_overlay.get_children():
            child.destroy()

    @property
    def view_type(self):
        """Return the type of view saved by the user"""
        view_type = settings.read_setting("view_type")
        if view_type in ["grid", "list"]:
            return view_type
        return self.default_view_type

    def do_key_press_event(self, event):  # pylint: disable=arguments-differ
        # XXX: This block of code below is to enable searching on type.
        # Enabling this feature steals focus from other entries so it needs
        # some kind of focus detection before enabling library search.

        # Probably not ideal for non-english, but we want to limit
        # which keys actually start searching
        if event.keyval == Gdk.KEY_Escape:
            self.search_entry.set_text("")
            self.view.grab_focus()
            return Gtk.ApplicationWindow.do_key_press_event(self, event)

        if (  # pylint: disable=too-many-boolean-expressions
            not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z or event.state & Gdk.ModifierType.CONTROL_MASK
            or event.state & Gdk.ModifierType.SHIFT_MASK or event.state & Gdk.ModifierType.META_MASK
            or event.state & Gdk.ModifierType.MOD1_MASK or self.search_entry.has_focus()
        ):
            return Gtk.ApplicationWindow.do_key_press_event(self, event)
        self.search_entry.grab_focus()
        return self.search_entry.do_key_press_event(self.search_entry, event)

    def load_icon_type(self):
        """Return the icon style depending on the type of view."""
        setting_key = "icon_type_%sview" % self.current_view_type
        if self.service and self.service.id != "lutris":
            setting_key += "_%s" % self.service.id
        self.icon_type = settings.read_setting(setting_key)
        return self.icon_type

    def save_icon_type(self, icon_type):
        """Save icon type to settings"""
        self.icon_type = icon_type
        setting_key = "icon_type_%sview" % self.current_view_type
        if self.service and self.service.id != "lutris":
            setting_key += "_%s" % self.service.id
        settings.write_setting(setting_key, self.icon_type)
        self.redraw_view()

    def redraw_view(self):
        """Completely reconstruct the main view"""
        if not self.game_store:
            logger.error("No game store yet")
            return
        if self.view:
            self.view.destroy()
        self.game_store = GameStore(self.service, self.service_media)
        if self.view_type == "grid":
            self.view = GameGridView(
                self.game_store,
                self.game_store.service_media,
                hide_text=settings.read_setting("hide_text_under_icons") == "True"
            )
        else:
            self.view = GameListView(self.game_store, self.game_store.service_media)

        self.view.connect("game-selected", self.on_game_selection_changed)
        self.view.connect("game-activated", self.on_game_activated)
        self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
        for child in self.games_scrollwindow.get_children():
            child.destroy()
        self.games_scrollwindow.add(self.view)

        self.view.show_all()
        self.update_store()

    def set_viewtype_icon(self, view_type):
        self.viewtype_icon.set_from_icon_name("view-%s-symbolic" % view_type, Gtk.IconSize.BUTTON)

    def set_show_installed_state(self, filter_installed):
        """Shows or hide uninstalled games"""
        settings.write_setting("filter_installed", bool(filter_installed))
        self.filters["installed"] = filter_installed

    def on_service_games_updated(self, service):
        """Request a view update when service games are loaded"""
        if self.service and service.id == self.service.id:
            self.emit("view-updated")
        return True

    def on_service_login(self, service):
        AsyncCall(service.reload, None)
        return True

    def on_service_logout(self, service):
        if self.service and service.id == self.service.id:
            self.emit("view-updated")
        return True

    @GtkTemplate.Callback
    def on_resize(self, widget, *_args):
        """Size-allocate signal.
        Updates stored window size and maximized state.
        """
        if not widget.get_window():
            return
        self.maximized = widget.is_maximized()
        size = widget.get_size()
        if not self.maximized:
            self.window_size = size
        self.search_entry.set_size_request(min(max(50, size[0] - 470), 800), -1)

    def on_window_delete(self, *_args):
        if self.application.running_games.get_n_items():
            self.hide()
            return True

    def on_window_configure(self, *_args):
        """Callback triggered when the window is moved, resized..."""
        self.window_x, self.window_y = self.get_position()

    @GtkTemplate.Callback
    def on_destroy(self, *_args):
        """Signal for window close."""
        # Stop cancellable running threads
        for stopper in self.threads_stoppers:
            stopper()

        # Save settings
        width, height = self.window_size
        settings.write_setting("width", width)
        settings.write_setting("height", height)
        if self.window_x and self.window_y:
            settings.write_setting("window_x", self.window_x)
            settings.write_setting("window_y", self.window_y)
        settings.write_setting("maximized", self.maximized)

    @GtkTemplate.Callback
    def on_preferences_activate(self, *_args):
        """Callback when preferences is activated."""
        self.application.show_window(PreferencesDialog)

    def on_show_installed_state_change(self, action, value):
        """Callback to handle uninstalled game filter switch"""
        action.set_state(value)
        self.set_show_installed_state(value.get_boolean())
        self.emit("view-updated")

    @GtkTemplate.Callback
    def on_search_entry_changed(self, entry):
        """Callback for the search input keypresses"""
        if self.search_timer_id:
            GLib.source_remove(self.search_timer_id)
        self.filters["text"] = entry.get_text().lower().strip()
        self.search_timer_id = GLib.timeout_add(150, self.update_store)

    @GtkTemplate.Callback
    def on_search_entry_key_press(self, widget, event):
        if event.keyval == Gdk.KEY_Down:
            if self.current_view_type == 'grid':
                self.view.select_path(Gtk.TreePath('0'))  # needed for gridview only
                # if game_bar is alive at this point it can mess grid item selection up
                # for some unknown reason,
                # it is safe to close it here, it will be reopened automatically.
                if self.game_bar:
                    self.game_bar.destroy()  # for gridview only
            self.view.set_cursor(Gtk.TreePath('0'), None, False)  # needed for both view types
            self.view.grab_focus()

    @GtkTemplate.Callback
    def on_about_clicked(self, *_args):
        """Open the about dialog."""
        dialogs.AboutDialog(parent=self)

    def on_game_error(self, game, error):
        """Called when a game has sent the 'game-error' signal"""
        logger.error("%s crashed", game)
        dialogs.ErrorDialog(error, parent=self)

    @GtkTemplate.Callback
    def on_add_game_button_clicked(self, *_args):
        """Add a new game manually with the AddGameDialog."""
        self.application.show_window(AddGamesWindow)
        return True

    def on_toggle_viewtype(self, *args):
        view_type = "list" if self.current_view_type == "grid" else "grid"
        logger.debug("View type changed to %s", view_type)
        self.set_viewtype_icon(view_type)
        settings.write_setting("view_type", view_type)
        self.redraw_view()
        self._bind_zoom_adjustment()

    def on_icontype_state_change(self, action, value):
        action.set_state(value)
        self._set_icon_type(value.get_string())

    def on_view_sorting_state_change(self, action, value):
        self.actions["view-sorting"].set_state(value)
        value = str(value).strip("'")
        settings.write_setting("view_sorting", value)
        self.emit("view-updated")

    def on_view_sorting_direction_change(self, action, value):
        self.actions["view-sorting-ascending"].set_state(value)
        settings.write_setting("view_sorting_ascending", bool(value))
        self.emit("view-updated")

    def on_side_panel_state_change(self, action, value):
        """Callback to handle side panel toggle"""
        action.set_state(value)
        side_panel_visible = value.get_boolean()
        settings.write_setting("side_panel_visible", bool(side_panel_visible))
        self.sidebar_revealer.set_reveal_child(side_panel_visible)

    def on_sidebar_changed(self, widget):
        """Handler called when the selected element of the sidebar changes"""
        for filter_type in ("category", "dynamic_category", "service", "runner", "platform"):
            if filter_type in self.filters:
                self.filters.pop(filter_type)

        row = widget.get_selected_row()
        if row:
            self.selected_category = "%s:%s" % (row.type, row.id)
            self.filters[row.type] = row.id

        service_name = self.filters.get("service")
        self.set_service(service_name)
        self._bind_zoom_adjustment()
        self.redraw_view()

    def on_game_selection_changed(self, view, selection):
        if not selection:
            GLib.idle_add(self.update_revealer)
            return False
        game_id = view.get_model().get_value(selection, COL_ID)
        if not game_id:
            GLib.idle_add(self.update_revealer)
            return False
        if self.service:
            game = ServiceGameCollection.get_game(self.service.id, game_id)
        else:
            game = games_db.get_game_by_field(int(game_id), "id")
        if not game:
            game = {
                "id": game_id,
                "appid": game_id,
                "name": view.get_model().get_value(selection, COL_NAME),
                "slug": game_id,
                "service": self.service.id if self.service else None,
            }
            logger.warning("No game found. Replacing with placeholder %s", game)

        GLib.idle_add(self.update_revealer, game)
        return False

    def is_game_displayed(self, game):
        """Return whether a game should be displayed on the view"""
        if game.is_hidden and not self.show_hidden_games:
            return False

        # Stopped games do not get displayed on the running page
        if game.state == game.STATE_STOPPED:
            selected_row = self.sidebar.get_selected_row()
            if selected_row and selected_row.id == "running":
                return False
        return True

    def on_game_updated(self, game):
        """Updates an individual entry in the view when a game is updated"""
        if game.appid and self.service:
            db_game = ServiceGameCollection.get_game(self.service.id, game.appid)
        else:
            db_game = games_db.get_game_by_field(game.id, "id")
        if not self.is_game_displayed(game):
            self.game_store.remove_game(db_game["id"])
            return True
        updated = self.game_store.update(db_game)
        if not updated:
            self.update_store()
        return True

    def on_game_stopped(self, game):
        """Updates the game list when a game stops; this keeps the 'running' page updated."""
        selected_row = self.sidebar.get_selected_row()
        # Only update the running page- we lose the selected row when we do this,
        # but on the running page this is okay.
        if selected_row is not None and selected_row.id == "running":
            self.game_store.remove_game(game.id)
        return True

    def on_game_collection_changed(self, _sender):
        """Simple method used to refresh the view"""
        self.emit("view-updated")
        return True

    def on_game_activated(self, view, game_id):
        """Handles view activations (double click, enter press)"""
        if self.service:
            logger.debug("Looking up %s game %s", self.service.id, game_id)
            db_game = games_db.get_game_for_service(self.service.id, game_id)
            if self.service.id == "lutris":
                if not db_game or not db_game["installed"]:
                    self.service.install(game_id)
                    return
                game_id = db_game["id"]
            else:
                if db_game and db_game["installed"]:
                    game_id = db_game["id"]
                else:
                    service_game = ServiceGameCollection.get_game(self.service.id, game_id)
                    if not service_game:
                        logger.error("No game %s found for %s", game_id, self.service.id)
                        return
                    game_id = self.service.install(service_game)
        if game_id:
            game = Game(game_id)
            if game.is_installed:
                game.emit("game-launch")
            else:
                game.emit("game-install")
__gtype_name__ special
blank_overlay
current_view_type property readonly

Returns which kind of view is currently presented (grid or list)

default_height
default_view_type
default_width
filter_installed property readonly
game_revealer
games_scrollwindow
search_entry
service_media property readonly
show_hidden_games property readonly
show_tray_icon property readonly

Setting to hide or show status icon

side_panel_visible property readonly
sidebar_revealer
sidebar_scrolled
sort_params property readonly
view_sorting property readonly
view_sorting_ascending property readonly
view_type property readonly

Return the type of view saved by the user

viewtype_icon
zoom_adjustment
__init__(self, application, **kwargs) special
Source code in lutris/gui/lutriswindow.py
def __init__(self, application, **kwargs):
    width = int(settings.read_setting("width") or self.default_width)
    height = int(settings.read_setting("height") or self.default_height)
    super().__init__(
        default_width=width,
        default_height=height,
        window_position=Gtk.WindowPosition.NONE,
        name="lutris",
        icon_name="lutris",
        application=application,
        **kwargs
    )
    update_desktop_icons()
    load_icon_theme()
    self.application = application
    self.window_x = settings.read_setting("window_x")
    self.window_y = settings.read_setting("window_y")
    if self.window_x and self.window_y:
        self.move(int(self.window_x), int(self.window_y))
    self.threads_stoppers = []
    self.window_size = (width, height)
    self.maximized = settings.read_setting("maximized") == "True"
    self.service = None
    self.game_actions = GameActions(application=application, window=self)
    self.search_timer_id = None
    self.selected_category = settings.read_setting("selected_category", default="runner:all")
    self.filters = self.load_filters()
    self.set_service(self.filters.get("service"))
    self.icon_type = self.load_icon_type()
    self.game_store = GameStore(self.service, self.service_media)
    self.view = Gtk.Box()

    self.connect("delete-event", self.on_window_delete)
    self.connect("configure-event", self.on_window_configure)
    self.connect("realize", self.on_load)
    if self.maximized:
        self.maximize()

    self.init_template()
    self._init_actions()

    self.set_viewtype_icon(self.view_type)

    lutris_icon = Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU)
    lutris_icon.set_margin_right(3)

    self.sidebar = LutrisSidebar(self.application, selected=self.selected_category)
    self.sidebar.connect("selected-rows-changed", self.on_sidebar_changed)
    # "realize" is order sensitive- must connect after sidebar itself connects the same signal
    self.sidebar.connect("realize", self.on_sidebar_realize)
    self.sidebar_scrolled.add(self.sidebar)

    # This must wait until the selected-rows-changed signal is connected
    self.sidebar.initialize_rows()

    self.sidebar_revealer.set_reveal_child(self.side_panel_visible)
    self.sidebar_revealer.set_transition_duration(300)

    self.game_bar = None
    self.revealer_box = Gtk.HBox(visible=True)
    self.game_revealer.add(self.revealer_box)

    self.connect("view-updated", self.update_store)
    GObject.add_emission_hook(BaseService, "service-login", self.on_service_login)
    GObject.add_emission_hook(BaseService, "service-logout", self.on_service_logout)
    GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
    GObject.add_emission_hook(Game, "game-updated", self.on_game_updated)
    GObject.add_emission_hook(Game, "game-stopped", self.on_game_stopped)
    GObject.add_emission_hook(Game, "game-removed", self.on_game_collection_changed)
combine_games(service_game, lutris_game) staticmethod

Inject lutris game information into a service game

Source code in lutris/gui/lutriswindow.py
@staticmethod
def combine_games(service_game, lutris_game):
    """Inject lutris game information into a service game"""
    if lutris_game and service_game["appid"] == lutris_game["service_id"]:
        for field in ("platform", "runner", "year", "installed_at", "lastplayed", "playtime", "installed"):
            service_game[field] = lutris_game[field]
    return service_game
do_key_press_event(self, event)

key_press_event(self, event:Gdk.EventKey) -> bool

Source code in lutris/gui/lutriswindow.py
def do_key_press_event(self, event):  # pylint: disable=arguments-differ
    # XXX: This block of code below is to enable searching on type.
    # Enabling this feature steals focus from other entries so it needs
    # some kind of focus detection before enabling library search.

    # Probably not ideal for non-english, but we want to limit
    # which keys actually start searching
    if event.keyval == Gdk.KEY_Escape:
        self.search_entry.set_text("")
        self.view.grab_focus()
        return Gtk.ApplicationWindow.do_key_press_event(self, event)

    if (  # pylint: disable=too-many-boolean-expressions
        not Gdk.KEY_0 <= event.keyval <= Gdk.KEY_z or event.state & Gdk.ModifierType.CONTROL_MASK
        or event.state & Gdk.ModifierType.SHIFT_MASK or event.state & Gdk.ModifierType.META_MASK
        or event.state & Gdk.ModifierType.MOD1_MASK or self.search_entry.has_focus()
    ):
        return Gtk.ApplicationWindow.do_key_press_event(self, event)
    self.search_entry.grab_focus()
    return self.search_entry.do_key_press_event(self.search_entry, event)
game_matches(self, game)
Source code in lutris/gui/lutriswindow.py
def game_matches(self, game):
    if self.filters.get("installed"):
        if game["appid"] not in games_db.get_service_games(self.service.id):
            return False
    if not self.filters.get("text"):
        return True
    return self.filters["text"] in game["name"].lower()
get_games_from_filters(self)
Source code in lutris/gui/lutriswindow.py
def get_games_from_filters(self):
    service_name = self.filters.get("service")
    if service_name in services.SERVICES:
        if self.service.online and not self.service.is_authenticated():
            self.show_label(_("Connect your %s account to access your games") % self.service.name)
            return []
        return self.get_service_games(service_name)
    dynamic_categories = {
        "recent": self.get_recent_games,
        "running": self.get_running_games,
    }
    if self.filters.get("dynamic_category") in dynamic_categories:
        return dynamic_categories[self.filters["dynamic_category"]]()
    if self.filters.get("category") and self.filters["category"] != "all":
        game_ids = categories_db.get_game_ids_for_category(self.filters["category"])
    else:
        game_ids = None
    searches, filters, excludes = self.get_sql_filters()
    games = games_db.get_games(
        searches=searches,
        filters=filters,
        excludes=excludes,
        sorts=self.sort_params
    )
    if game_ids is not None:
        return [game for game in games if game["id"] in game_ids]
    return games
get_recent_games(self)

Return a list of currently running games

Source code in lutris/gui/lutriswindow.py
def get_recent_games(self):
    """Return a list of currently running games"""
    searches, _filters, excludes = self.get_sql_filters()
    games = games_db.get_games(searches=searches, filters={'installed': '1'}, excludes=excludes)
    return sorted(
        games,
        key=lambda game: max(game["installed_at"] or 0, game["lastplayed"] or 0),
        reverse=True
    )
get_running_games(self)

Return a list of currently running games

Source code in lutris/gui/lutriswindow.py
def get_running_games(self):
    """Return a list of currently running games"""
    return games_db.get_games_by_ids([game.id for game in self.application.running_games])
get_service_games(self, service_name)

Switch the current service to service_name and return games if available

Source code in lutris/gui/lutriswindow.py
def get_service_games(self, service_name):
    """Switch the current service to service_name and return games if available"""
    service_games = ServiceGameCollection.get_for_service(service_name)
    if service_name == "lutris":
        lutris_games = {g["slug"]: g for g in games_db.get_games()}
    else:
        lutris_games = {g["service_id"]: g for g in games_db.get_games(filters={"service": self.service.id})}

    def get_sort_value(game):
        sort_defaults = {
            "name": "",
            "year": 0,
            "lastplayed": 0.0,
            "installed_at": 0.0,
            "playtime": 0.0,
        }
        view_sorting = self.view_sorting
        lutris_game = lutris_games.get(game["appid"])
        if not lutris_game:
            return sort_defaults.get(view_sorting, "")
        value = lutris_game.get(view_sorting)
        if value:
            return value
        # Users may have obsolete view_sorting settings, so
        # we must tolerate them. We treat them all as blank.
        return sort_defaults.get(view_sorting, "")

    return [
        self.combine_games(game, lutris_games.get(game["appid"])) for game in sorted(
            service_games,
            key=get_sort_value,
            reverse=not self.view_sorting_ascending
        ) if self.game_matches(game)
    ]
get_service_media(self, icon_type)

Return the ServiceMedia class used for this view

Source code in lutris/gui/lutriswindow.py
def get_service_media(self, icon_type):
    """Return the ServiceMedia class used for this view"""
    service = self.service if self.service else LutrisService
    medias = service.medias
    if icon_type in medias:
        return medias[icon_type]()
    return medias[service.default_format]()
get_sql_filters(self)

Return the current filters for the view

Source code in lutris/gui/lutriswindow.py
def get_sql_filters(self):
    """Return the current filters for the view"""
    sql_filters = {}
    sql_excludes = {}
    if self.filters.get("runner"):
        sql_filters["runner"] = self.filters["runner"]
    if self.filters.get("platform"):
        sql_filters["platform"] = self.filters["platform"]
    if self.filters.get("installed"):
        sql_filters["installed"] = "1"
    if self.filters.get("text"):
        searches = {"name": self.filters["text"]}
    else:
        searches = None
    if not self.filters.get("hidden"):
        sql_excludes["hidden"] = 1
    return searches, sql_filters, sql_excludes
hidden_state_change(self, action, value)

Hides or shows the hidden games

Source code in lutris/gui/lutriswindow.py
def hidden_state_change(self, action, value):
    """Hides or shows the hidden games"""
    action.set_state(value)
    settings.write_setting("show_hidden_games", str(value).lower(), section="lutris")
    self.filters["hidden"] = value
    self.emit("view-updated")
hide_overlay(self)
Source code in lutris/gui/lutriswindow.py
def hide_overlay(self):
    self.blank_overlay.props.visible = False
    for child in self.blank_overlay.get_children():
        child.destroy()
init_template(s)
Source code in lutris/gui/lutriswindow.py
cls.init_template = lambda s: _init_template(s, cls, base_init_template)
is_game_displayed(self, game)

Return whether a game should be displayed on the view

Source code in lutris/gui/lutriswindow.py
def is_game_displayed(self, game):
    """Return whether a game should be displayed on the view"""
    if game.is_hidden and not self.show_hidden_games:
        return False

    # Stopped games do not get displayed on the running page
    if game.state == game.STATE_STOPPED:
        selected_row = self.sidebar.get_selected_row()
        if selected_row and selected_row.id == "running":
            return False
    return True
load_filters(self)

Load the initial filters when creating the view

Source code in lutris/gui/lutriswindow.py
def load_filters(self):
    """Load the initial filters when creating the view"""
    category, value = self.selected_category.split(":")
    filters = {
        category: value
    }  # Type of filter corresponding to the selected sidebar element
    filters["hidden"] = settings.read_setting("show_hidden_games").lower() == "true"
    filters["installed"] = settings.read_setting("filter_installed").lower() == "true"
    return filters
load_icon_type(self)

Return the icon style depending on the type of view.

Source code in lutris/gui/lutriswindow.py
def load_icon_type(self):
    """Return the icon style depending on the type of view."""
    setting_key = "icon_type_%sview" % self.current_view_type
    if self.service and self.service.id != "lutris":
        setting_key += "_%s" % self.service.id
    self.icon_type = settings.read_setting(setting_key)
    return self.icon_type
on_about_clicked(self, *_args)

Open the about dialog.

Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_about_clicked(self, *_args):
    """Open the about dialog."""
    dialogs.AboutDialog(parent=self)
on_add_game_button_clicked(self, *_args)

Add a new game manually with the AddGameDialog.

Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_add_game_button_clicked(self, *_args):
    """Add a new game manually with the AddGameDialog."""
    self.application.show_window(AddGamesWindow)
    return True
on_destroy(self, *_args)

Signal for window close.

Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_destroy(self, *_args):
    """Signal for window close."""
    # Stop cancellable running threads
    for stopper in self.threads_stoppers:
        stopper()

    # Save settings
    width, height = self.window_size
    settings.write_setting("width", width)
    settings.write_setting("height", height)
    if self.window_x and self.window_y:
        settings.write_setting("window_x", self.window_x)
        settings.write_setting("window_y", self.window_y)
    settings.write_setting("maximized", self.maximized)
on_game_activated(self, view, game_id)

Handles view activations (double click, enter press)

Source code in lutris/gui/lutriswindow.py
def on_game_activated(self, view, game_id):
    """Handles view activations (double click, enter press)"""
    if self.service:
        logger.debug("Looking up %s game %s", self.service.id, game_id)
        db_game = games_db.get_game_for_service(self.service.id, game_id)
        if self.service.id == "lutris":
            if not db_game or not db_game["installed"]:
                self.service.install(game_id)
                return
            game_id = db_game["id"]
        else:
            if db_game and db_game["installed"]:
                game_id = db_game["id"]
            else:
                service_game = ServiceGameCollection.get_game(self.service.id, game_id)
                if not service_game:
                    logger.error("No game %s found for %s", game_id, self.service.id)
                    return
                game_id = self.service.install(service_game)
    if game_id:
        game = Game(game_id)
        if game.is_installed:
            game.emit("game-launch")
        else:
            game.emit("game-install")
on_game_collection_changed(self, _sender)

Simple method used to refresh the view

Source code in lutris/gui/lutriswindow.py
def on_game_collection_changed(self, _sender):
    """Simple method used to refresh the view"""
    self.emit("view-updated")
    return True
on_game_error(self, game, error)

Called when a game has sent the 'game-error' signal

Source code in lutris/gui/lutriswindow.py
def on_game_error(self, game, error):
    """Called when a game has sent the 'game-error' signal"""
    logger.error("%s crashed", game)
    dialogs.ErrorDialog(error, parent=self)
on_game_selection_changed(self, view, selection)
Source code in lutris/gui/lutriswindow.py
def on_game_selection_changed(self, view, selection):
    if not selection:
        GLib.idle_add(self.update_revealer)
        return False
    game_id = view.get_model().get_value(selection, COL_ID)
    if not game_id:
        GLib.idle_add(self.update_revealer)
        return False
    if self.service:
        game = ServiceGameCollection.get_game(self.service.id, game_id)
    else:
        game = games_db.get_game_by_field(int(game_id), "id")
    if not game:
        game = {
            "id": game_id,
            "appid": game_id,
            "name": view.get_model().get_value(selection, COL_NAME),
            "slug": game_id,
            "service": self.service.id if self.service else None,
        }
        logger.warning("No game found. Replacing with placeholder %s", game)

    GLib.idle_add(self.update_revealer, game)
    return False
on_game_stopped(self, game)

Updates the game list when a game stops; this keeps the 'running' page updated.

Source code in lutris/gui/lutriswindow.py
def on_game_stopped(self, game):
    """Updates the game list when a game stops; this keeps the 'running' page updated."""
    selected_row = self.sidebar.get_selected_row()
    # Only update the running page- we lose the selected row when we do this,
    # but on the running page this is okay.
    if selected_row is not None and selected_row.id == "running":
        self.game_store.remove_game(game.id)
    return True
on_game_updated(self, game)

Updates an individual entry in the view when a game is updated

Source code in lutris/gui/lutriswindow.py
def on_game_updated(self, game):
    """Updates an individual entry in the view when a game is updated"""
    if game.appid and self.service:
        db_game = ServiceGameCollection.get_game(self.service.id, game.appid)
    else:
        db_game = games_db.get_game_by_field(game.id, "id")
    if not self.is_game_displayed(game):
        self.game_store.remove_game(db_game["id"])
        return True
    updated = self.game_store.update(db_game)
    if not updated:
        self.update_store()
    return True
on_icontype_state_change(self, action, value)
Source code in lutris/gui/lutriswindow.py
def on_icontype_state_change(self, action, value):
    action.set_state(value)
    self._set_icon_type(value.get_string())
on_load(self, widget, data=None)

Finish initializing the view

Source code in lutris/gui/lutriswindow.py
def on_load(self, widget, data=None):
    """Finish initializing the view"""
    self._bind_zoom_adjustment()
    self.view.grab_focus()
    self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
on_preferences_activate(self, *_args)

Callback when preferences is activated.

Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_preferences_activate(self, *_args):
    """Callback when preferences is activated."""
    self.application.show_window(PreferencesDialog)
on_resize(self, widget, *_args)

Size-allocate signal. Updates stored window size and maximized state.

Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_resize(self, widget, *_args):
    """Size-allocate signal.
    Updates stored window size and maximized state.
    """
    if not widget.get_window():
        return
    self.maximized = widget.is_maximized()
    size = widget.get_size()
    if not self.maximized:
        self.window_size = size
    self.search_entry.set_size_request(min(max(50, size[0] - 470), 800), -1)
on_search_entry_changed(self, entry)

Callback for the search input keypresses

Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_search_entry_changed(self, entry):
    """Callback for the search input keypresses"""
    if self.search_timer_id:
        GLib.source_remove(self.search_timer_id)
    self.filters["text"] = entry.get_text().lower().strip()
    self.search_timer_id = GLib.timeout_add(150, self.update_store)
on_search_entry_key_press(self, widget, event)
Source code in lutris/gui/lutriswindow.py
@GtkTemplate.Callback
def on_search_entry_key_press(self, widget, event):
    if event.keyval == Gdk.KEY_Down:
        if self.current_view_type == 'grid':
            self.view.select_path(Gtk.TreePath('0'))  # needed for gridview only
            # if game_bar is alive at this point it can mess grid item selection up
            # for some unknown reason,
            # it is safe to close it here, it will be reopened automatically.
            if self.game_bar:
                self.game_bar.destroy()  # for gridview only
        self.view.set_cursor(Gtk.TreePath('0'), None, False)  # needed for both view types
        self.view.grab_focus()
on_service_games_updated(self, service)

Request a view update when service games are loaded

Source code in lutris/gui/lutriswindow.py
def on_service_games_updated(self, service):
    """Request a view update when service games are loaded"""
    if self.service and service.id == self.service.id:
        self.emit("view-updated")
    return True
on_service_login(self, service)
Source code in lutris/gui/lutriswindow.py
def on_service_login(self, service):
    AsyncCall(service.reload, None)
    return True
on_service_logout(self, service)
Source code in lutris/gui/lutriswindow.py
def on_service_logout(self, service):
    if self.service and service.id == self.service.id:
        self.emit("view-updated")
    return True
on_show_installed_state_change(self, action, value)

Callback to handle uninstalled game filter switch

Source code in lutris/gui/lutriswindow.py
def on_show_installed_state_change(self, action, value):
    """Callback to handle uninstalled game filter switch"""
    action.set_state(value)
    self.set_show_installed_state(value.get_boolean())
    self.emit("view-updated")
on_side_panel_state_change(self, action, value)

Callback to handle side panel toggle

Source code in lutris/gui/lutriswindow.py
def on_side_panel_state_change(self, action, value):
    """Callback to handle side panel toggle"""
    action.set_state(value)
    side_panel_visible = value.get_boolean()
    settings.write_setting("side_panel_visible", bool(side_panel_visible))
    self.sidebar_revealer.set_reveal_child(side_panel_visible)
on_sidebar_changed(self, widget)

Handler called when the selected element of the sidebar changes

Source code in lutris/gui/lutriswindow.py
def on_sidebar_changed(self, widget):
    """Handler called when the selected element of the sidebar changes"""
    for filter_type in ("category", "dynamic_category", "service", "runner", "platform"):
        if filter_type in self.filters:
            self.filters.pop(filter_type)

    row = widget.get_selected_row()
    if row:
        self.selected_category = "%s:%s" % (row.type, row.id)
        self.filters[row.type] = row.id

    service_name = self.filters.get("service")
    self.set_service(service_name)
    self._bind_zoom_adjustment()
    self.redraw_view()
on_sidebar_realize(self, widget, data=None)

Grab the initial focus after the sidebar is initialized - so the view is ready.

Source code in lutris/gui/lutriswindow.py
def on_sidebar_realize(self, widget, data=None):
    """Grab the initial focus after the sidebar is initialized - so the view is ready."""
    self.view.grab_focus()
on_toggle_viewtype(self, *args)
Source code in lutris/gui/lutriswindow.py
def on_toggle_viewtype(self, *args):
    view_type = "list" if self.current_view_type == "grid" else "grid"
    logger.debug("View type changed to %s", view_type)
    self.set_viewtype_icon(view_type)
    settings.write_setting("view_type", view_type)
    self.redraw_view()
    self._bind_zoom_adjustment()
on_view_sorting_direction_change(self, action, value)
Source code in lutris/gui/lutriswindow.py
def on_view_sorting_direction_change(self, action, value):
    self.actions["view-sorting-ascending"].set_state(value)
    settings.write_setting("view_sorting_ascending", bool(value))
    self.emit("view-updated")
on_view_sorting_state_change(self, action, value)
Source code in lutris/gui/lutriswindow.py
def on_view_sorting_state_change(self, action, value):
    self.actions["view-sorting"].set_state(value)
    value = str(value).strip("'")
    settings.write_setting("view_sorting", value)
    self.emit("view-updated")
on_window_configure(self, *_args)

Callback triggered when the window is moved, resized...

Source code in lutris/gui/lutriswindow.py
def on_window_configure(self, *_args):
    """Callback triggered when the window is moved, resized..."""
    self.window_x, self.window_y = self.get_position()
on_window_delete(self, *_args)
Source code in lutris/gui/lutriswindow.py
def on_window_delete(self, *_args):
    if self.application.running_games.get_n_items():
        self.hide()
        return True
on_zoom_changed(self, adjustment)

Handler for zoom modification

Source code in lutris/gui/lutriswindow.py
def on_zoom_changed(self, adjustment):
    """Handler for zoom modification"""
    media_index = round(adjustment.props.value)
    adjustment.props.value = media_index
    service = self.service if self.service else LutrisService
    media_services = list(service.medias.keys())
    if len(media_services) <= media_index:
        media_index = media_services.index(service.default_format)
    icon_type = media_services[media_index]
    if icon_type != self.icon_type:
        self.save_icon_type(icon_type)
        self.show_spinner()
redraw_view(self)

Completely reconstruct the main view

Source code in lutris/gui/lutriswindow.py
def redraw_view(self):
    """Completely reconstruct the main view"""
    if not self.game_store:
        logger.error("No game store yet")
        return
    if self.view:
        self.view.destroy()
    self.game_store = GameStore(self.service, self.service_media)
    if self.view_type == "grid":
        self.view = GameGridView(
            self.game_store,
            self.game_store.service_media,
            hide_text=settings.read_setting("hide_text_under_icons") == "True"
        )
    else:
        self.view = GameListView(self.game_store, self.game_store.service_media)

    self.view.connect("game-selected", self.on_game_selection_changed)
    self.view.connect("game-activated", self.on_game_activated)
    self.view.contextual_menu = ContextualMenu(self.game_actions.get_game_actions())
    for child in self.games_scrollwindow.get_children():
        child.destroy()
    self.games_scrollwindow.add(self.view)

    self.view.show_all()
    self.update_store()
save_icon_type(self, icon_type)

Save icon type to settings

Source code in lutris/gui/lutriswindow.py
def save_icon_type(self, icon_type):
    """Save icon type to settings"""
    self.icon_type = icon_type
    setting_key = "icon_type_%sview" % self.current_view_type
    if self.service and self.service.id != "lutris":
        setting_key += "_%s" % self.service.id
    settings.write_setting(setting_key, self.icon_type)
    self.redraw_view()
set_service(self, service_name)
Source code in lutris/gui/lutriswindow.py
def set_service(self, service_name):
    if self.service and self.service.id == service_name:
        return self.service
    if not service_name:
        self.service = None
        return
    try:
        self.service = services.SERVICES[service_name]()
    except KeyError:
        logger.error("Non existent service '%s'", service_name)
        self.service = None
    return self.service
set_show_installed_state(self, filter_installed)

Shows or hide uninstalled games

Source code in lutris/gui/lutriswindow.py
def set_show_installed_state(self, filter_installed):
    """Shows or hide uninstalled games"""
    settings.write_setting("filter_installed", bool(filter_installed))
    self.filters["installed"] = filter_installed
set_viewtype_icon(self, view_type)
Source code in lutris/gui/lutriswindow.py
def set_viewtype_icon(self, view_type):
    self.viewtype_icon.set_from_icon_name("view-%s-symbolic" % view_type, Gtk.IconSize.BUTTON)
show_empty_label(self)

Display a label when the view is empty

Source code in lutris/gui/lutriswindow.py
def show_empty_label(self):
    """Display a label when the view is empty"""
    if self.filters.get("text"):
        self.show_label(_("No games matching '%s' found ") % self.filters["text"])
    else:
        if self.filters.get("category") == "favorite":
            self.show_label(_("Add games to your favorites to see them here."))
        elif self.filters.get("installed"):
            self.show_label(_("No installed games found. Press Ctrl+H so show all games."))
        else:
            self.show_splash()
            # self.show_label(_("No games found"))
show_label(self, message)

Display a label in the middle of the UI

Source code in lutris/gui/lutriswindow.py
def show_label(self, message):
    """Display a label in the middle of the UI"""
    self.show_overlay(Gtk.Label(message, visible=True))
show_overlay(self, widget, halign=<enum GTK_ALIGN_FILL of type Gtk.Align>, valign=<enum GTK_ALIGN_FILL of type Gtk.Align>)

Display a widget in the blank overlay

Source code in lutris/gui/lutriswindow.py
def show_overlay(self, widget, halign=Gtk.Align.FILL, valign=Gtk.Align.FILL):
    """Display a widget in the blank overlay"""
    for child in self.blank_overlay.get_children():
        child.destroy()
    self.blank_overlay.set_halign(halign)
    self.blank_overlay.set_valign(valign)
    self.blank_overlay.add(widget)
    self.blank_overlay.props.visible = True
show_spinner(self)
Source code in lutris/gui/lutriswindow.py
def show_spinner(self):
    spinner = Gtk.Spinner(visible=True)
    spinner.start()
    for child in self.blank_overlay.get_children():
        child.destroy()
    self.blank_overlay.add(spinner)
    self.blank_overlay.props.visible = True
show_splash(self)
Source code in lutris/gui/lutriswindow.py
def show_splash(self):
    image = Gtk.Image(visible=True)
    image.set_from_file(os.path.join(datapath.get(), "media/splash.svg"))
    self.show_overlay(image, Gtk.Align.START, Gtk.Align.START)
update_revealer(self, game=None)
Source code in lutris/gui/lutriswindow.py
def update_revealer(self, game=None):
    if game:
        if self.game_bar:
            self.game_bar.destroy()
        self.game_bar = GameBar(game, self.game_actions, self.application)
        self.revealer_box.pack_start(self.game_bar, True, True, 0)
    elif self.game_bar:
        # The game bar can't be destroyed here because the game gets unselected on Wayland
        # whenever the game bar is interacted with. Instead, we keep the current game bar open
        # when the game gets unselected, which is somewhat closer to what the intended behavior
        # should be anyway. Might require closing the game bar manually in some cases.
        pass
        # self.game_bar.destroy()
    if self.revealer_box.get_children():
        self.game_revealer.set_reveal_child(True)
    else:
        self.game_revealer.set_reveal_child(False)
update_store(self, *_args, **_kwargs)
Source code in lutris/gui/lutriswindow.py
def update_store(self, *_args, **_kwargs):
    self.game_store.store.clear()
    for child in self.blank_overlay.get_children():
        child.destroy()
    games = self.get_games_from_filters()
    logger.debug("Showing %d games", len(games))
    self.view.service = self.service.id if self.service else None
    GLib.idle_add(self.update_revealer)
    for game in games:
        self.game_store.add_game(game)
    if not games:
        self.show_empty_label()
    self.search_timer_id = None
    return False

views special

Common values used for views

COLUMN_NAMES

COL_ICON

COL_ID

COL_INSTALLED

COL_INSTALLED_AT

COL_INSTALLED_AT_TEXT

COL_LASTPLAYED

COL_LASTPLAYED_TEXT

COL_NAME

COL_PLATFORM

COL_PLAYTIME

COL_PLAYTIME_TEXT

COL_RUNNER

COL_RUNNER_HUMAN_NAME

COL_SLUG

COL_YEAR

base

GameView
Source code in lutris/gui/views/base.py
class GameView:
    # pylint: disable=no-member
    __gsignals__ = {
        "game-selected": (GObject.SIGNAL_RUN_FIRST, None, (Gtk.TreeIter, )),
        "game-activated": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
        "remove-game": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self):
        self.service = None  # Stores the service.id in a string
        self.current_path = None
        self.contextual_menu = None

    def connect_signals(self):
        """Signal handlers common to all views"""
        self.connect("button-press-event", self.popup_contextual_menu)
        self.connect("key-press-event", self.handle_key_press)

    def popup_contextual_menu(self, view, event):
        """Contextual menu."""
        if event.button != 3:
            return
        view.current_path = view.get_path_at_pos(event.x, event.y)
        if view.current_path:
            view.select()
            _iter = self.get_model().get_iter(view.current_path[0])
            if not _iter:
                return
            selected_id = self.get_selected_id(_iter)
            game_row = self.game_store.get_row_by_id(selected_id)
            game_id = None
            if self.service:
                game = get_game_for_service(self.service, game_row[COL_ID])
                if game:
                    game_id = game["id"]
            else:
                game_id = game_row[COL_ID]
            if not game_id:
                return
            game = Game(game_id)
            game_actions = GameActions()
            game_actions.set_game(game=game)

            self.contextual_menu.popup(event, game_actions)

    def get_selected_id(self, selected_item):
        return self.get_model().get_value(selected_item, COL_ID)

    def select(self):
        """Selects the object pointed by current_path"""
        raise NotImplementedError

    def handle_key_press(self, widget, event):  # pylint: disable=unused-argument
        key = event.keyval
        if key == Gdk.KEY_Delete:
            self.emit("remove-game")
__gsignals__ special
__init__(self) special
Source code in lutris/gui/views/base.py
def __init__(self):
    self.service = None  # Stores the service.id in a string
    self.current_path = None
    self.contextual_menu = None
connect_signals(self)

Signal handlers common to all views

Source code in lutris/gui/views/base.py
def connect_signals(self):
    """Signal handlers common to all views"""
    self.connect("button-press-event", self.popup_contextual_menu)
    self.connect("key-press-event", self.handle_key_press)
get_selected_id(self, selected_item)
Source code in lutris/gui/views/base.py
def get_selected_id(self, selected_item):
    return self.get_model().get_value(selected_item, COL_ID)
handle_key_press(self, widget, event)
Source code in lutris/gui/views/base.py
def handle_key_press(self, widget, event):  # pylint: disable=unused-argument
    key = event.keyval
    if key == Gdk.KEY_Delete:
        self.emit("remove-game")
popup_contextual_menu(self, view, event)

Contextual menu.

Source code in lutris/gui/views/base.py
def popup_contextual_menu(self, view, event):
    """Contextual menu."""
    if event.button != 3:
        return
    view.current_path = view.get_path_at_pos(event.x, event.y)
    if view.current_path:
        view.select()
        _iter = self.get_model().get_iter(view.current_path[0])
        if not _iter:
            return
        selected_id = self.get_selected_id(_iter)
        game_row = self.game_store.get_row_by_id(selected_id)
        game_id = None
        if self.service:
            game = get_game_for_service(self.service, game_row[COL_ID])
            if game:
                game_id = game["id"]
        else:
            game_id = game_row[COL_ID]
        if not game_id:
            return
        game = Game(game_id)
        game_actions = GameActions()
        game_actions.set_game(game=game)

        self.contextual_menu.popup(event, game_actions)
select(self)

Selects the object pointed by current_path

Source code in lutris/gui/views/base.py
def select(self):
    """Selects the object pointed by current_path"""
    raise NotImplementedError

grid

Grid view for the main window

GameGridView (IconView, GameView)
Source code in lutris/gui/views/grid.py
class GameGridView(Gtk.IconView, GameView):
    __gsignals__ = GameView.__gsignals__

    min_width = 70  # Minimum width for a cell

    def __init__(self, store, service_media, hide_text=False):
        self.game_store = store
        self.service_media = service_media
        self.model = self.game_store.store
        super().__init__(model=self.game_store.store)
        GameView.__init__(self)

        self.service = None
        self.set_column_spacing(6)
        self.set_pixbuf_column(COL_ICON)
        self.set_item_padding(1)
        self.cell_width = max(service_media.size[0], self.min_width)
        if hide_text:
            self.cell_renderer = None
        else:
            self.cell_renderer = GridViewCellRendererText(self.cell_width)
            self.pack_end(self.cell_renderer, False)
            self.add_attribute(self.cell_renderer, "markup", COL_NAME)

        self.connect_signals()
        self.connect("item-activated", self.on_item_activated)
        self.connect("selection-changed", self.on_selection_changed)

    def select(self):
        self.select_path(self.current_path)

    def get_selected_item(self):
        """Return the currently selected game's id."""
        selection = self.get_selected_items()
        if not selection:
            return
        self.current_path = selection[0]
        return self.get_model().get_iter(self.current_path)

    def on_item_activated(self, _view, _path):
        """Handles double clicks"""
        selected_item = self.get_selected_item()
        if selected_item:
            selected_id = self.get_selected_id(selected_item)
        else:
            selected_id = None
        logger.debug("Item activated: %s", selected_id)
        self.emit("game-activated", selected_id)

    def on_selection_changed(self, _view):
        """Handles selection changes"""
        selected_items = self.get_selected_item()
        if selected_items:
            self.emit("game-selected", selected_items)
min_width
__init__(self, store, service_media, hide_text=False) special
Source code in lutris/gui/views/grid.py
def __init__(self, store, service_media, hide_text=False):
    self.game_store = store
    self.service_media = service_media
    self.model = self.game_store.store
    super().__init__(model=self.game_store.store)
    GameView.__init__(self)

    self.service = None
    self.set_column_spacing(6)
    self.set_pixbuf_column(COL_ICON)
    self.set_item_padding(1)
    self.cell_width = max(service_media.size[0], self.min_width)
    if hide_text:
        self.cell_renderer = None
    else:
        self.cell_renderer = GridViewCellRendererText(self.cell_width)
        self.pack_end(self.cell_renderer, False)
        self.add_attribute(self.cell_renderer, "markup", COL_NAME)

    self.connect_signals()
    self.connect("item-activated", self.on_item_activated)
    self.connect("selection-changed", self.on_selection_changed)
get_selected_item(self)

Return the currently selected game's id.

Source code in lutris/gui/views/grid.py
def get_selected_item(self):
    """Return the currently selected game's id."""
    selection = self.get_selected_items()
    if not selection:
        return
    self.current_path = selection[0]
    return self.get_model().get_iter(self.current_path)
on_item_activated(self, _view, _path)

Handles double clicks

Source code in lutris/gui/views/grid.py
def on_item_activated(self, _view, _path):
    """Handles double clicks"""
    selected_item = self.get_selected_item()
    if selected_item:
        selected_id = self.get_selected_id(selected_item)
    else:
        selected_id = None
    logger.debug("Item activated: %s", selected_id)
    self.emit("game-activated", selected_id)
on_selection_changed(self, _view)

Handles selection changes

Source code in lutris/gui/views/grid.py
def on_selection_changed(self, _view):
    """Handles selection changes"""
    selected_items = self.get_selected_item()
    if selected_items:
        self.emit("game-selected", selected_items)
select(self)

Selects the object pointed by current_path

Source code in lutris/gui/views/grid.py
def select(self):
    self.select_path(self.current_path)

list

TreeView based game list

GameListColumnToggleMenu (Menu)
Source code in lutris/gui/views/list.py
class GameListColumnToggleMenu(Gtk.Menu):

    def __init__(self, columns):
        super().__init__()
        self.columns = columns
        self.column_map = {}
        self.create_menuitems()
        self.show_all()

    def create_menuitems(self):
        for column in self.columns:
            title = column.get_title()
            if title == "":
                continue
            checkbox = Gtk.CheckMenuItem(title)
            checkbox.set_active(column.get_visible())
            if title == _("Name"):
                checkbox.set_sensitive(False)
            else:
                checkbox.connect("toggled", self.on_toggle_column)
            self.column_map[checkbox] = column
            self.append(checkbox)

    def on_toggle_column(self, check_menu_item):
        column = self.column_map[check_menu_item]
        is_visible = check_menu_item.get_active()
        column.set_visible(is_visible)
        settings.write_setting(
            column.get_title().replace(" ", "") + "_visible",
            str(is_visible),
            "list view",
        )
__init__(self, columns) special
Source code in lutris/gui/views/list.py
def __init__(self, columns):
    super().__init__()
    self.columns = columns
    self.column_map = {}
    self.create_menuitems()
    self.show_all()
create_menuitems(self)
Source code in lutris/gui/views/list.py
def create_menuitems(self):
    for column in self.columns:
        title = column.get_title()
        if title == "":
            continue
        checkbox = Gtk.CheckMenuItem(title)
        checkbox.set_active(column.get_visible())
        if title == _("Name"):
            checkbox.set_sensitive(False)
        else:
            checkbox.connect("toggled", self.on_toggle_column)
        self.column_map[checkbox] = column
        self.append(checkbox)
on_toggle_column(self, check_menu_item)
Source code in lutris/gui/views/list.py
def on_toggle_column(self, check_menu_item):
    column = self.column_map[check_menu_item]
    is_visible = check_menu_item.get_active()
    column.set_visible(is_visible)
    settings.write_setting(
        column.get_title().replace(" ", "") + "_visible",
        str(is_visible),
        "list view",
    )
GameListView (TreeView, GameView)

Show the main list of games.

Source code in lutris/gui/views/list.py
class GameListView(Gtk.TreeView, GameView):

    """Show the main list of games."""

    __gsignals__ = GameView.__gsignals__

    def __init__(self, store, service_media):
        self.game_store = store
        self.service_media = service_media
        self.model = self.game_store.store
        super().__init__(model=self.model)
        GameView.__init__(self)
        self.set_rules_hint(True)

        # Icon column
        if settings.SHOW_MEDIA:
            image_cell = Gtk.CellRendererPixbuf()
            column = Gtk.TreeViewColumn("", image_cell, pixbuf=COL_ICON)
            column.set_reorderable(True)
            column.set_sort_indicator(False)
            self.append_column(column)

        # Text columns
        default_text_cell = self.set_text_cell()
        name_cell = self.set_text_cell()
        name_cell.set_padding(5, 0)

        self.set_column(name_cell, _("Name"), COL_NAME, 200, always_visible=True)
        self.set_column(default_text_cell, _("Year"), COL_YEAR, 60)
        self.set_column(default_text_cell, _("Runner"), COL_RUNNER_HUMAN_NAME, 120)
        self.set_column(default_text_cell, _("Platform"), COL_PLATFORM, 120)
        self.set_column(default_text_cell, _("Last Played"), COL_LASTPLAYED_TEXT, 120)
        self.set_sort_with_column(COL_LASTPLAYED_TEXT, COL_LASTPLAYED)
        self.set_column(default_text_cell, _("Installed At"), COL_INSTALLED_AT_TEXT, 120)
        self.set_sort_with_column(COL_INSTALLED_AT_TEXT, COL_INSTALLED_AT)
        self.set_column(default_text_cell, _("Play Time"), COL_PLAYTIME_TEXT, 100)
        self.set_sort_with_column(COL_PLAYTIME_TEXT, COL_PLAYTIME)

        self.get_selection().set_mode(Gtk.SelectionMode.SINGLE)

        self.connect_signals()
        self.connect("row-activated", self.on_row_activated)
        self.get_selection().connect("changed", self.on_cursor_changed)

    @staticmethod
    def set_text_cell():
        text_cell = Gtk.CellRendererText()
        text_cell.set_padding(10, 0)
        text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
        return text_cell

    def set_column(self, cell, header, column_id, default_width, always_visible=False, sort_id=None):
        column = Gtk.TreeViewColumn(header, cell, markup=column_id)
        column.set_sort_indicator(True)
        column.set_sort_column_id(column_id if sort_id is None else sort_id)
        self.set_column_sort(column_id if sort_id is None else sort_id)
        column.set_resizable(True)
        column.set_reorderable(True)
        width = settings.read_setting("%s_column_width" % COLUMN_NAMES[column_id], "list view")
        is_visible = settings.read_setting("%s_visible" % COLUMN_NAMES[column_id], "list view")
        column.set_fixed_width(int(width) if width else default_width)
        column.set_visible(is_visible == "True" or always_visible if is_visible else True)
        self.append_column(column)
        column.connect("notify::width", self.on_column_width_changed)
        column.get_button().connect('button-press-event', self.on_column_header_button_pressed)
        return column

    def set_column_sort(self, col):
        """Sort a column and fallback to sorting by name and runner."""
        model = self.get_model()
        if model:
            model.set_sort_func(col, sort_func, col)

    def set_sort_with_column(self, col, sort_col):
        """Sort a column by using another column's data"""
        self.model.set_sort_func(col, sort_func, sort_col)

    def get_selected_item(self):
        """Return the currently selected game's id."""
        selection = self.get_selection()
        if not selection:
            return None
        _model, select_iter = selection.get_selected()
        if select_iter:
            return select_iter

    def select(self):
        self.set_cursor(self.current_path[0])

    def set_selected_game(self, game_id):
        row = self.game_store.get_row_by_id(game_id, filtered=True)
        if row:
            self.set_cursor(row.path)

    def on_column_header_button_pressed(self, button, event):
        """Handles column header button press events"""
        if event.button == 3:
            menu = GameListColumnToggleMenu(self.get_columns())
            menu.popup_at_pointer(None)
            return True

    def on_row_activated(self, widget, line=None, column=None):
        """Handles double clicks"""
        selected_item = self.get_selected_item()
        if selected_item:
            selected_id = self.get_selected_id(selected_item)
        else:
            selected_id = None
        self.emit("game-activated", selected_id)

    def on_cursor_changed(self, widget, _line=None, _column=None):
        selected_item = self.get_selected_item()
        self.emit("game-selected", selected_item)

    @staticmethod
    def on_column_width_changed(col, *args):
        col_name = col.get_title()
        if col_name:
            settings.write_setting(
                col_name.replace(" ", "") + "_column_width",
                col.get_fixed_width(),
                "list view",
            )
__init__(self, store, service_media) special
Source code in lutris/gui/views/list.py
def __init__(self, store, service_media):
    self.game_store = store
    self.service_media = service_media
    self.model = self.game_store.store
    super().__init__(model=self.model)
    GameView.__init__(self)
    self.set_rules_hint(True)

    # Icon column
    if settings.SHOW_MEDIA:
        image_cell = Gtk.CellRendererPixbuf()
        column = Gtk.TreeViewColumn("", image_cell, pixbuf=COL_ICON)
        column.set_reorderable(True)
        column.set_sort_indicator(False)
        self.append_column(column)

    # Text columns
    default_text_cell = self.set_text_cell()
    name_cell = self.set_text_cell()
    name_cell.set_padding(5, 0)

    self.set_column(name_cell, _("Name"), COL_NAME, 200, always_visible=True)
    self.set_column(default_text_cell, _("Year"), COL_YEAR, 60)
    self.set_column(default_text_cell, _("Runner"), COL_RUNNER_HUMAN_NAME, 120)
    self.set_column(default_text_cell, _("Platform"), COL_PLATFORM, 120)
    self.set_column(default_text_cell, _("Last Played"), COL_LASTPLAYED_TEXT, 120)
    self.set_sort_with_column(COL_LASTPLAYED_TEXT, COL_LASTPLAYED)
    self.set_column(default_text_cell, _("Installed At"), COL_INSTALLED_AT_TEXT, 120)
    self.set_sort_with_column(COL_INSTALLED_AT_TEXT, COL_INSTALLED_AT)
    self.set_column(default_text_cell, _("Play Time"), COL_PLAYTIME_TEXT, 100)
    self.set_sort_with_column(COL_PLAYTIME_TEXT, COL_PLAYTIME)

    self.get_selection().set_mode(Gtk.SelectionMode.SINGLE)

    self.connect_signals()
    self.connect("row-activated", self.on_row_activated)
    self.get_selection().connect("changed", self.on_cursor_changed)
get_selected_item(self)

Return the currently selected game's id.

Source code in lutris/gui/views/list.py
def get_selected_item(self):
    """Return the currently selected game's id."""
    selection = self.get_selection()
    if not selection:
        return None
    _model, select_iter = selection.get_selected()
    if select_iter:
        return select_iter
on_column_header_button_pressed(self, button, event)

Handles column header button press events

Source code in lutris/gui/views/list.py
def on_column_header_button_pressed(self, button, event):
    """Handles column header button press events"""
    if event.button == 3:
        menu = GameListColumnToggleMenu(self.get_columns())
        menu.popup_at_pointer(None)
        return True
on_column_width_changed(col, *args) staticmethod
Source code in lutris/gui/views/list.py
@staticmethod
def on_column_width_changed(col, *args):
    col_name = col.get_title()
    if col_name:
        settings.write_setting(
            col_name.replace(" ", "") + "_column_width",
            col.get_fixed_width(),
            "list view",
        )
on_cursor_changed(self, widget, _line=None, _column=None)
Source code in lutris/gui/views/list.py
def on_cursor_changed(self, widget, _line=None, _column=None):
    selected_item = self.get_selected_item()
    self.emit("game-selected", selected_item)
on_row_activated(self, widget, line=None, column=None)

Handles double clicks

Source code in lutris/gui/views/list.py
def on_row_activated(self, widget, line=None, column=None):
    """Handles double clicks"""
    selected_item = self.get_selected_item()
    if selected_item:
        selected_id = self.get_selected_id(selected_item)
    else:
        selected_id = None
    self.emit("game-activated", selected_id)
select(self)

Selects the object pointed by current_path

Source code in lutris/gui/views/list.py
def select(self):
    self.set_cursor(self.current_path[0])
set_column(self, cell, header, column_id, default_width, always_visible=False, sort_id=None)
Source code in lutris/gui/views/list.py
def set_column(self, cell, header, column_id, default_width, always_visible=False, sort_id=None):
    column = Gtk.TreeViewColumn(header, cell, markup=column_id)
    column.set_sort_indicator(True)
    column.set_sort_column_id(column_id if sort_id is None else sort_id)
    self.set_column_sort(column_id if sort_id is None else sort_id)
    column.set_resizable(True)
    column.set_reorderable(True)
    width = settings.read_setting("%s_column_width" % COLUMN_NAMES[column_id], "list view")
    is_visible = settings.read_setting("%s_visible" % COLUMN_NAMES[column_id], "list view")
    column.set_fixed_width(int(width) if width else default_width)
    column.set_visible(is_visible == "True" or always_visible if is_visible else True)
    self.append_column(column)
    column.connect("notify::width", self.on_column_width_changed)
    column.get_button().connect('button-press-event', self.on_column_header_button_pressed)
    return column
set_column_sort(self, col)

Sort a column and fallback to sorting by name and runner.

Source code in lutris/gui/views/list.py
def set_column_sort(self, col):
    """Sort a column and fallback to sorting by name and runner."""
    model = self.get_model()
    if model:
        model.set_sort_func(col, sort_func, col)
set_selected_game(self, game_id)
Source code in lutris/gui/views/list.py
def set_selected_game(self, game_id):
    row = self.game_store.get_row_by_id(game_id, filtered=True)
    if row:
        self.set_cursor(row.path)
set_sort_with_column(self, col, sort_col)

Sort a column by using another column's data

Source code in lutris/gui/views/list.py
def set_sort_with_column(self, col, sort_col):
    """Sort a column by using another column's data"""
    self.model.set_sort_func(col, sort_func, sort_col)
set_text_cell() staticmethod
Source code in lutris/gui/views/list.py
@staticmethod
def set_text_cell():
    text_cell = Gtk.CellRendererText()
    text_cell.set_padding(10, 0)
    text_cell.set_property("ellipsize", Pango.EllipsizeMode.END)
    return text_cell

media_loader

Loads game media in parallel

download_media(media_urls, service_media)

Download a list of media files concurrently.

Limits the number of simultaneous downloads to avoid API throttling and UI being overloaded with signals.

Source code in lutris/gui/views/media_loader.py
def download_media(media_urls, service_media):
    """Download a list of media files concurrently.

    Limits the number of simultaneous downloads to avoid API throttling
    and UI being overloaded with signals.
    """
    icons = {}
    num_workers = 5
    with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers) as executor:
        future_downloads = {
            executor.submit(service_media.download, slug, url): slug
            for slug, url in media_urls.items()
            if url
        }
        for future in concurrent.futures.as_completed(future_downloads):
            slug = future_downloads[future]
            try:
                path = future.result()
            except Exception as ex:  # pylint: disable=broad-except
                logger.exception('%r failed: %s', slug, ex)
                path = None
            if system.path_exists(path):
                icons[slug] = path
    return icons

store

Store object for a list of games

GameStore (Object)
Source code in lutris/gui/views/store.py
class GameStore(GObject.Object):
    __gsignals__ = {
        "icons-changed": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, service, service_media):
        super().__init__()
        self.service = service
        self.service_media = service_media
        self._installed_games = []
        self._installed_games_accessed = False
        self._icon_updates = {}

        self.store = Gtk.ListStore(
            str,
            str,
            str,
            Pixbuf,
            str,
            str,
            str,
            str,
            int,
            str,
            bool,
            int,
            str,
            float,
            str,
        )

    @property
    def installed_game_slugs(self):
        previous_access = self._installed_games_accessed or 0
        self._installed_games_accessed = time.time()
        if self._installed_games_accessed - previous_access > 1:
            self._installed_games = [g["slug"] for g in get_games(filters={"installed": "1"})]
        return self._installed_games

    def add_games(self, games):
        """Add games to the store"""
        for game in list(games):
            GLib.idle_add(self.add_game, game)

    def get_row_by_slug(self, slug):
        for model_row in self.store:
            if model_row[COL_SLUG] == slug:
                return model_row

    def get_row_by_id(self, _id):
        if not _id:
            return
        for model_row in self.store:
            try:
                if model_row[COL_ID] == str(_id):
                    return model_row
            except TypeError:
                return

    def remove_game(self, _id):
        """Remove a game from the view."""
        row = self.get_row_by_id(_id)
        if row:
            self.store.remove(row.iter)

    def update(self, db_game):
        """Update game informations
        Return whether a row was updated; False if the game was not already
        present.
        """
        store_item = StoreItem(db_game, self.service_media)
        row = self.get_row_by_id(store_item.id)
        if not row:
            row = self.get_row_by_id(db_game["service_id"])
        if not row:
            return False
        row[COL_ID] = str(store_item.id)
        row[COL_SLUG] = store_item.slug
        row[COL_NAME] = gtk_safe(store_item.name)
        if settings.SHOW_MEDIA:
            row[COL_ICON] = store_item.get_pixbuf()
        else:
            row[COL_ICON] = None
        row[COL_YEAR] = store_item.year
        row[COL_RUNNER] = store_item.runner
        row[COL_RUNNER_HUMAN_NAME] = gtk_safe(store_item.runner_text)
        row[COL_PLATFORM] = gtk_safe(store_item.platform)
        row[COL_LASTPLAYED] = store_item.lastplayed
        row[COL_LASTPLAYED_TEXT] = store_item.lastplayed_text
        row[COL_INSTALLED] = store_item.installed
        row[COL_INSTALLED_AT] = store_item.installed_at
        row[COL_INSTALLED_AT_TEXT] = store_item.installed_at_text
        row[COL_PLAYTIME] = store_item.playtime
        row[COL_PLAYTIME_TEXT] = store_item.playtime_text
        return True

    def add_game(self, db_game):
        """Add a PGA game to the store"""
        game = StoreItem(db_game, self.service_media)
        self.store.append(
            (
                str(game.id),
                game.slug,
                game.name,
                game.get_pixbuf() if settings.SHOW_MEDIA else None,
                game.year,
                game.runner,
                game.runner_text,
                gtk_safe(game.platform),
                game.lastplayed,
                game.lastplayed_text,
                game.installed,
                game.installed_at,
                game.installed_at_text,
                game.playtime,
                game.playtime_text,
            )
        )

    def on_game_updated(self, game):
        if self.service:
            db_games = sql.filtered_query(
                settings.PGA_DB,
                "service_games",
                filters=({
                    "service": self.service_media.service,
                    "appid": game.appid
                })
            )
        else:
            db_games = sql.filtered_query(
                settings.PGA_DB,
                "games",
                filters=({
                    "id": game.id
                })
            )

        for db_game in db_games:
            GLib.idle_add(self.update, db_game)
        return True
installed_game_slugs property readonly
__init__(self, service, service_media) special
Source code in lutris/gui/views/store.py
def __init__(self, service, service_media):
    super().__init__()
    self.service = service
    self.service_media = service_media
    self._installed_games = []
    self._installed_games_accessed = False
    self._icon_updates = {}

    self.store = Gtk.ListStore(
        str,
        str,
        str,
        Pixbuf,
        str,
        str,
        str,
        str,
        int,
        str,
        bool,
        int,
        str,
        float,
        str,
    )
add_game(self, db_game)

Add a PGA game to the store

Source code in lutris/gui/views/store.py
def add_game(self, db_game):
    """Add a PGA game to the store"""
    game = StoreItem(db_game, self.service_media)
    self.store.append(
        (
            str(game.id),
            game.slug,
            game.name,
            game.get_pixbuf() if settings.SHOW_MEDIA else None,
            game.year,
            game.runner,
            game.runner_text,
            gtk_safe(game.platform),
            game.lastplayed,
            game.lastplayed_text,
            game.installed,
            game.installed_at,
            game.installed_at_text,
            game.playtime,
            game.playtime_text,
        )
    )
add_games(self, games)

Add games to the store

Source code in lutris/gui/views/store.py
def add_games(self, games):
    """Add games to the store"""
    for game in list(games):
        GLib.idle_add(self.add_game, game)
get_row_by_id(self, _id)
Source code in lutris/gui/views/store.py
def get_row_by_id(self, _id):
    if not _id:
        return
    for model_row in self.store:
        try:
            if model_row[COL_ID] == str(_id):
                return model_row
        except TypeError:
            return
get_row_by_slug(self, slug)
Source code in lutris/gui/views/store.py
def get_row_by_slug(self, slug):
    for model_row in self.store:
        if model_row[COL_SLUG] == slug:
            return model_row
on_game_updated(self, game)
Source code in lutris/gui/views/store.py
def on_game_updated(self, game):
    if self.service:
        db_games = sql.filtered_query(
            settings.PGA_DB,
            "service_games",
            filters=({
                "service": self.service_media.service,
                "appid": game.appid
            })
        )
    else:
        db_games = sql.filtered_query(
            settings.PGA_DB,
            "games",
            filters=({
                "id": game.id
            })
        )

    for db_game in db_games:
        GLib.idle_add(self.update, db_game)
    return True
remove_game(self, _id)

Remove a game from the view.

Source code in lutris/gui/views/store.py
def remove_game(self, _id):
    """Remove a game from the view."""
    row = self.get_row_by_id(_id)
    if row:
        self.store.remove(row.iter)
update(self, db_game)

Update game informations Return whether a row was updated; False if the game was not already present.

Source code in lutris/gui/views/store.py
def update(self, db_game):
    """Update game informations
    Return whether a row was updated; False if the game was not already
    present.
    """
    store_item = StoreItem(db_game, self.service_media)
    row = self.get_row_by_id(store_item.id)
    if not row:
        row = self.get_row_by_id(db_game["service_id"])
    if not row:
        return False
    row[COL_ID] = str(store_item.id)
    row[COL_SLUG] = store_item.slug
    row[COL_NAME] = gtk_safe(store_item.name)
    if settings.SHOW_MEDIA:
        row[COL_ICON] = store_item.get_pixbuf()
    else:
        row[COL_ICON] = None
    row[COL_YEAR] = store_item.year
    row[COL_RUNNER] = store_item.runner
    row[COL_RUNNER_HUMAN_NAME] = gtk_safe(store_item.runner_text)
    row[COL_PLATFORM] = gtk_safe(store_item.platform)
    row[COL_LASTPLAYED] = store_item.lastplayed
    row[COL_LASTPLAYED_TEXT] = store_item.lastplayed_text
    row[COL_INSTALLED] = store_item.installed
    row[COL_INSTALLED_AT] = store_item.installed_at
    row[COL_INSTALLED_AT_TEXT] = store_item.installed_at_text
    row[COL_PLAYTIME] = store_item.playtime
    row[COL_PLAYTIME_TEXT] = store_item.playtime_text
    return True
sort_func(model, row1, row2, sort_col)

Sorting function for the game store

Source code in lutris/gui/views/store.py
def sort_func(model, row1, row2, sort_col):
    """Sorting function for the game store"""
    value1 = model.get_value(row1, sort_col)
    value2 = model.get_value(row2, sort_col)
    if value1 is None and value2 is None:
        value1 = value2 = 0
    elif value1 is None:
        value1 = type(value2)()
    elif value2 is None:
        value2 = type(value1)()
    value1 = try_lower(value1)
    value2 = try_lower(value2)
    diff = -1 if value1 < value2 else 0 if value1 == value2 else 1
    if diff == 0:
        value1 = try_lower(model.get_value(row1, COL_NAME))
        value2 = try_lower(model.get_value(row2, COL_NAME))
        try:
            diff = -1 if value1 < value2 else 0 if value1 == value2 else 1
        except TypeError:
            diff = 0
    if diff == 0:
        value1 = try_lower(model.get_value(row1, COL_RUNNER_HUMAN_NAME))
        value2 = try_lower(model.get_value(row2, COL_RUNNER_HUMAN_NAME))
    try:
        return -1 if value1 < value2 else 0 if value1 == value2 else 1
    except TypeError:
        return 0
try_lower(value)
Source code in lutris/gui/views/store.py
def try_lower(value):
    try:
        out = value.lower()
    except AttributeError:
        out = value
    return out

store_item

Game representation for views

StoreItem

Representation of a game for views TODO: Fix overlap with Game class

Source code in lutris/gui/views/store_item.py
class StoreItem:
    """Representation of a game for views
    TODO: Fix overlap with Game class
    """

    def __init__(self, game_data, service_media):
        if not game_data:
            raise RuntimeError("No game data provided")
        self._game_data = game_data
        self.service_media = service_media

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<Store id=%s slug=%s>" % (self.id, self.slug)

    @property
    def id(self):  # pylint: disable=invalid-name
        """Game internal ID"""
        # Return an unique identifier for the game.
        # Since service games are not related to lutris, use the appid
        if "service_id" not in self._game_data:
            if "appid" in self._game_data:
                return self._game_data["appid"]
            return self._game_data["slug"]

        return self._game_data["id"]

    @property
    def service(self):
        return gtk_safe(self._game_data.get("service"))

    @property
    def slug(self):
        """Slug identifier"""
        return gtk_safe(self._game_data["slug"])

    @property
    def name(self):
        """Name"""
        return gtk_safe(self._game_data["name"])

    @property
    def year(self):
        """Year"""
        return str(self._game_data.get("year") or "")

    @property
    def runner(self):
        """Runner slug"""
        return gtk_safe(self._game_data.get("runner")) or ""

    @property
    def runner_text(self):
        """Runner name"""
        return gtk_safe(RUNNER_NAMES.get(self.runner))

    @property
    def platform(self):
        """Platform"""
        _platform = self._game_data.get("platform")
        if not _platform and not self.service and self.installed:
            game_inst = Game(self._game_data["id"])
            if game_inst.platform:
                _platform = game_inst.platform
        return gtk_safe(_platform)

    @property
    def installed(self):
        """Game is installed"""
        if "service_id" not in self._game_data:
            return self.id in get_service_games(self.service)
        if not self._game_data.get("runner"):
            return False
        return self._game_data.get("installed")

    def get_pixbuf(self):
        """Pixbuf varying on icon type"""
        if self._game_data.get("icon"):
            image_path = self._game_data["icon"]
        else:
            image_path = self.service_media.get_absolute_path(self.slug)
            if not system.path_exists(image_path):
                service = self._game_data.get("service")
                appid = self._game_data.get("service_id")
                if appid:
                    service_game = ServiceGameCollection.get_game(service, appid)
                else:
                    service_game = None
                if service_game:
                    image_path = self.service_media.get_absolute_path(service_game["slug"])
        if system.path_exists(image_path):
            return get_pixbuf(image_path, self.service_media.size, is_installed=self.installed)
        return self.service_media.get_pixbuf_for_game(
            self._game_data["slug"],
            self.installed
        )

    @property
    def installed_at(self):
        """Date of install"""
        return self._game_data.get("installed_at")

    @property
    def installed_at_text(self):
        """Date of install (textual representation)"""
        return gtk_safe(
            time.strftime("%X %x", time.localtime(self.installed_at)) if
            self.installed_at else ""
        )

    @property
    def lastplayed(self):
        """Date of last play"""
        return self._game_data.get("lastplayed")

    @property
    def lastplayed_text(self):
        """Date of last play (textual representation)"""
        return gtk_safe(
            time.strftime(
                "%X %x",
                time.localtime(self.lastplayed)
            ) if self.lastplayed else ""
        )

    @property
    def playtime(self):
        """Playtime duration in hours"""
        try:
            return float(self._game_data.get("playtime", 0))
        except (TypeError, ValueError):
            return 0.0

    @property
    def playtime_text(self):
        """Playtime duration in hours (textual representation)"""
        try:
            _playtime_text = get_formatted_playtime(self.playtime)
        except ValueError:
            logger.warning("Invalid playtime value %s for %s", self.playtime, self)
            _playtime_text = ""  # Do not show erroneous values
        return gtk_safe(_playtime_text)
id property readonly

Game internal ID

installed property readonly

Game is installed

installed_at property readonly

Date of install

installed_at_text property readonly

Date of install (textual representation)

lastplayed property readonly

Date of last play

lastplayed_text property readonly

Date of last play (textual representation)

name property readonly

Name

platform property readonly

Platform

playtime property readonly

Playtime duration in hours

playtime_text property readonly

Playtime duration in hours (textual representation)

runner property readonly

Runner slug

runner_text property readonly

Runner name

service property readonly
slug property readonly

Slug identifier

year property readonly

Year

__init__(self, game_data, service_media) special
Source code in lutris/gui/views/store_item.py
def __init__(self, game_data, service_media):
    if not game_data:
        raise RuntimeError("No game data provided")
    self._game_data = game_data
    self.service_media = service_media
__repr__(self) special
Source code in lutris/gui/views/store_item.py
def __repr__(self):
    return "<Store id=%s slug=%s>" % (self.id, self.slug)
__str__(self) special
Source code in lutris/gui/views/store_item.py
def __str__(self):
    return self.name
get_pixbuf(self)

Pixbuf varying on icon type

Source code in lutris/gui/views/store_item.py
def get_pixbuf(self):
    """Pixbuf varying on icon type"""
    if self._game_data.get("icon"):
        image_path = self._game_data["icon"]
    else:
        image_path = self.service_media.get_absolute_path(self.slug)
        if not system.path_exists(image_path):
            service = self._game_data.get("service")
            appid = self._game_data.get("service_id")
            if appid:
                service_game = ServiceGameCollection.get_game(service, appid)
            else:
                service_game = None
            if service_game:
                image_path = self.service_media.get_absolute_path(service_game["slug"])
    if system.path_exists(image_path):
        return get_pixbuf(image_path, self.service_media.size, is_installed=self.installed)
    return self.service_media.get_pixbuf_for_game(
        self._game_data["slug"],
        self.installed
    )

widgets special

cellrenderers

GridViewCellRendererText (CellRendererText)

CellRendererText adjusted for grid view display, removes extra padding

Source code in lutris/gui/widgets/cellrenderers.py
class GridViewCellRendererText(Gtk.CellRendererText):
    """CellRendererText adjusted for grid view display, removes extra padding"""

    def __init__(self, width, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.props.alignment = Pango.Alignment.CENTER
        self.props.wrap_mode = Pango.WrapMode.WORD
        self.props.xalign = 0.5
        self.props.yalign = 0
        self.props.wrap_width = width
__init__(self, width, *args, **kwargs) special
Source code in lutris/gui/widgets/cellrenderers.py
def __init__(self, width, *args, **kwargs):
    super().__init__(*args, **kwargs)
    self.props.alignment = Pango.Alignment.CENTER
    self.props.wrap_mode = Pango.WrapMode.WORD
    self.props.xalign = 0.5
    self.props.yalign = 0
    self.props.wrap_width = width

common

Misc widgets used in the GUI.

EditableGrid (Grid)
Source code in lutris/gui/widgets/common.py
class EditableGrid(Gtk.Grid):
    __gsignals__ = {"changed": (GObject.SIGNAL_RUN_FIRST, None, ())}

    def __init__(self, data, columns):
        self.columns = columns
        super().__init__()
        self.set_column_homogeneous(True)
        self.set_row_homogeneous(True)
        self.set_row_spacing(10)
        self.set_column_spacing(10)

        self.liststore = Gtk.ListStore(str, str)
        for item in data:
            self.liststore.append([str(value) for value in item])

        self.treeview = Gtk.TreeView.new_with_model(self.liststore)
        self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
        for i, column_title in enumerate(self.columns):
            renderer = Gtk.CellRendererText()
            renderer.set_property("editable", True)
            renderer.connect("edited", self.on_text_edited, i)

            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
            column.set_resizable(True)
            column.set_min_width(100)
            column.set_sort_column_id(0)
            self.treeview.append_column(column)

        self.buttons = []
        self.add_button = Gtk.Button(_("Add"))
        self.buttons.append(self.add_button)
        self.add_button.connect("clicked", self.on_add)

        self.delete_button = Gtk.Button(_("Delete"))
        self.buttons.append(self.delete_button)
        self.delete_button.connect("clicked", self.on_delete)

        self.scrollable_treelist = Gtk.ScrolledWindow()
        self.scrollable_treelist.set_vexpand(True)
        self.scrollable_treelist.add(self.treeview)

        self.attach(self.scrollable_treelist, 0, 0, 5, 5)
        self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)
        for i, button in enumerate(self.buttons[1:]):
            self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
        self.show_all()

    def on_add(self, widget):  # pylint: disable=unused-argument
        self.liststore.append(["", ""])
        row_position = len(self.liststore) - 1
        self.treeview.set_cursor(row_position, None, False)
        self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)
        self.emit("changed")

    def on_delete(self, widget):  # pylint: disable=unused-argument
        selection = self.treeview.get_selection()
        _, iteration = selection.get_selected()
        self.liststore.remove(iteration)
        self.emit("changed")

    def on_text_edited(self, widget, path, text, field):  # pylint: disable=unused-argument
        self.liststore[path][field] = text.strip()  # pylint: disable=unsubscriptable-object
        self.emit("changed")

    def get_data(self):  # pylint: disable=arguments-differ
        model_data = []
        for row in self.liststore:  # pylint: disable=not-an-iterable
            model_data.append(row)
        return model_data
__init__(self, data, columns) special
Source code in lutris/gui/widgets/common.py
def __init__(self, data, columns):
    self.columns = columns
    super().__init__()
    self.set_column_homogeneous(True)
    self.set_row_homogeneous(True)
    self.set_row_spacing(10)
    self.set_column_spacing(10)

    self.liststore = Gtk.ListStore(str, str)
    for item in data:
        self.liststore.append([str(value) for value in item])

    self.treeview = Gtk.TreeView.new_with_model(self.liststore)
    self.treeview.set_grid_lines(Gtk.TreeViewGridLines.BOTH)
    for i, column_title in enumerate(self.columns):
        renderer = Gtk.CellRendererText()
        renderer.set_property("editable", True)
        renderer.connect("edited", self.on_text_edited, i)

        column = Gtk.TreeViewColumn(column_title, renderer, text=i)
        column.set_resizable(True)
        column.set_min_width(100)
        column.set_sort_column_id(0)
        self.treeview.append_column(column)

    self.buttons = []
    self.add_button = Gtk.Button(_("Add"))
    self.buttons.append(self.add_button)
    self.add_button.connect("clicked", self.on_add)

    self.delete_button = Gtk.Button(_("Delete"))
    self.buttons.append(self.delete_button)
    self.delete_button.connect("clicked", self.on_delete)

    self.scrollable_treelist = Gtk.ScrolledWindow()
    self.scrollable_treelist.set_vexpand(True)
    self.scrollable_treelist.add(self.treeview)

    self.attach(self.scrollable_treelist, 0, 0, 5, 5)
    self.attach(self.add_button, 5 - len(self.buttons), 6, 1, 1)
    for i, button in enumerate(self.buttons[1:]):
        self.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
    self.show_all()
get_data(self)

get_data(self, key:str)

Source code in lutris/gui/widgets/common.py
def get_data(self):  # pylint: disable=arguments-differ
    model_data = []
    for row in self.liststore:  # pylint: disable=not-an-iterable
        model_data.append(row)
    return model_data
on_add(self, widget)
Source code in lutris/gui/widgets/common.py
def on_add(self, widget):  # pylint: disable=unused-argument
    self.liststore.append(["", ""])
    row_position = len(self.liststore) - 1
    self.treeview.set_cursor(row_position, None, False)
    self.treeview.scroll_to_cell(row_position, None, False, 0.0, 0.0)
    self.emit("changed")
on_delete(self, widget)
Source code in lutris/gui/widgets/common.py
def on_delete(self, widget):  # pylint: disable=unused-argument
    selection = self.treeview.get_selection()
    _, iteration = selection.get_selected()
    self.liststore.remove(iteration)
    self.emit("changed")
on_text_edited(self, widget, path, text, field)
Source code in lutris/gui/widgets/common.py
def on_text_edited(self, widget, path, text, field):  # pylint: disable=unused-argument
    self.liststore[path][field] = text.strip()  # pylint: disable=unsubscriptable-object
    self.emit("changed")
FileChooserEntry (Box)

Editable entry with a file picker button

Source code in lutris/gui/widgets/common.py
class FileChooserEntry(Gtk.Box):

    """Editable entry with a file picker button"""

    max_completion_items = 15  # Maximum number of items to display in the autocompletion dropdown.

    def __init__(
        self,
        title=_("Select file"),
        action=Gtk.FileChooserAction.OPEN,
        path=None,
        default_path=None,
        warn_if_non_empty=False,
        warn_if_ntfs=False
    ):
        super().__init__(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=0,
            visible=True
        )
        self.title = title
        self.action = action
        self.path = os.path.expanduser(path) if path else None
        self.default_path = os.path.expanduser(default_path) if default_path else path
        self.warn_if_non_empty = warn_if_non_empty
        self.warn_if_ntfs = warn_if_ntfs

        self.path_completion = Gtk.ListStore(str)

        self.entry = Gtk.Entry(visible=True)
        self.entry.set_completion(self.get_completion())
        self.entry.connect("changed", self.on_entry_changed)
        if path:
            self.entry.set_text(path)

        browse_button = Gtk.Button(_("Browse..."), visible=True)
        browse_button.connect("clicked", self.on_browse_clicked)

        box = Gtk.Box(spacing=6, visible=True)
        box.pack_start(self.entry, True, True, 0)
        box.add(browse_button)
        self.pack_start(box, False, False, 0)

    def get_text(self):
        """Return the entry's text"""
        return self.entry.get_text()

    def get_filename(self):
        """Deprecated"""
        logger.warning("Just use get_text")
        return self.get_text()

    def get_completion(self):
        """Return an EntryCompletion widget"""
        completion = Gtk.EntryCompletion()
        completion.set_model(self.path_completion)
        completion.set_text_column(0)
        return completion

    def get_filechooser_dialog(self):
        """Return an instance of a FileChooserNative configured for this widget"""
        dialog = Gtk.FileChooserNative.new(self.title, None, self.action, _("_OK"), _("_Cancel"))
        dialog.set_create_folders(True)
        dialog.set_current_folder(self.get_default_folder())
        dialog.connect("response", self.on_select_file, dialog)
        return dialog

    def get_default_folder(self):
        """Return the default folder for the file picker"""
        default_path = self.path or self.default_path or ""
        if not default_path or not system.path_exists(default_path):
            current_entry = self.get_text()
            if system.path_exists(current_entry):
                default_path = current_entry
        if not os.path.isdir(default_path):
            default_path = os.path.dirname(default_path)
        return os.path.expanduser(default_path or "~")

    def on_browse_clicked(self, _widget):
        """Browse button click callback"""
        file_chooser_dialog = self.get_filechooser_dialog()
        file_chooser_dialog.show()

    def on_entry_changed(self, widget):
        """Entry changed callback"""
        self.clear_warnings()
        path = widget.get_text()
        if not path:
            return
        path = os.path.expanduser(path)
        self.update_completion(path)
        self.path = path
        if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == "ntfs":
            ntfs_box = Gtk.Box(spacing=6, visible=True)
            warning_image = Gtk.Image(visible=True)
            warning_image.set_from_pixbuf(get_stock_icon("dialog-warning", 32))
            ntfs_box.add(warning_image)
            ntfs_label = Gtk.Label(visible=True)
            ntfs_label.set_markup(_(
                "<b>Warning!</b> The selected path is located on a drive formatted by Windows.\n"
                "Games and programs installed on Windows drives usually <b>don't work</b>."
            ))
            ntfs_box.add(ntfs_label)
            self.pack_end(ntfs_box, False, False, 10)
        if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):
            non_empty_label = Gtk.Label(visible=True)
            non_empty_label.set_markup(_(
                "<b>Warning!</b> The selected path "
                "contains files. Installation might not work properly."
            ))
            self.pack_end(non_empty_label, False, False, 10)
        parent = system.get_existing_parent(path)
        if parent is not None and not os.access(parent, os.W_OK):
            non_writable_destination_label = Gtk.Label(visible=True)
            non_writable_destination_label.set_markup(_(
                "<b>Warning</b> The destination folder "
                "is not writable by the current user."
            ))
            self.pack_end(non_writable_destination_label, False, False, 10)

    def on_select_file(self, dialog, response, _dialog):
        """FileChooserDialog response callback"""
        if response == Gtk.ResponseType.ACCEPT:
            target_path = dialog.get_filename()
            if target_path:
                dialog.set_current_folder(target_path)
                self.entry.set_text(system.reverse_expanduser(target_path))
        dialog.destroy()

    def update_completion(self, current_path):
        """Update the auto-completion widget with the current path"""
        self.path_completion.clear()

        if not os.path.exists(current_path):
            current_path, filefilter = os.path.split(current_path)
        else:
            filefilter = None

        if os.path.isdir(current_path):
            index = 0
            for filename in sorted(os.listdir(current_path)):
                if filename.startswith("."):
                    continue
                if filefilter is not None and not filename.startswith(filefilter):
                    continue
                self.path_completion.append([os.path.join(current_path, filename)])
                index += 1
                if index > self.max_completion_items:
                    break

    def clear_warnings(self):
        """Delete all the warning labels from the container"""
        for index, child in enumerate(self.get_children()):
            if index > 0:
                child.destroy()
max_completion_items
__init__(self, title='Select file', action=<enum GTK_FILE_CHOOSER_ACTION_OPEN of type Gtk.FileChooserAction>, path=None, default_path=None, warn_if_non_empty=False, warn_if_ntfs=False) special
Source code in lutris/gui/widgets/common.py
def __init__(
    self,
    title=_("Select file"),
    action=Gtk.FileChooserAction.OPEN,
    path=None,
    default_path=None,
    warn_if_non_empty=False,
    warn_if_ntfs=False
):
    super().__init__(
        orientation=Gtk.Orientation.VERTICAL,
        spacing=0,
        visible=True
    )
    self.title = title
    self.action = action
    self.path = os.path.expanduser(path) if path else None
    self.default_path = os.path.expanduser(default_path) if default_path else path
    self.warn_if_non_empty = warn_if_non_empty
    self.warn_if_ntfs = warn_if_ntfs

    self.path_completion = Gtk.ListStore(str)

    self.entry = Gtk.Entry(visible=True)
    self.entry.set_completion(self.get_completion())
    self.entry.connect("changed", self.on_entry_changed)
    if path:
        self.entry.set_text(path)

    browse_button = Gtk.Button(_("Browse..."), visible=True)
    browse_button.connect("clicked", self.on_browse_clicked)

    box = Gtk.Box(spacing=6, visible=True)
    box.pack_start(self.entry, True, True, 0)
    box.add(browse_button)
    self.pack_start(box, False, False, 0)
clear_warnings(self)

Delete all the warning labels from the container

Source code in lutris/gui/widgets/common.py
def clear_warnings(self):
    """Delete all the warning labels from the container"""
    for index, child in enumerate(self.get_children()):
        if index > 0:
            child.destroy()
get_completion(self)

Return an EntryCompletion widget

Source code in lutris/gui/widgets/common.py
def get_completion(self):
    """Return an EntryCompletion widget"""
    completion = Gtk.EntryCompletion()
    completion.set_model(self.path_completion)
    completion.set_text_column(0)
    return completion
get_default_folder(self)

Return the default folder for the file picker

Source code in lutris/gui/widgets/common.py
def get_default_folder(self):
    """Return the default folder for the file picker"""
    default_path = self.path or self.default_path or ""
    if not default_path or not system.path_exists(default_path):
        current_entry = self.get_text()
        if system.path_exists(current_entry):
            default_path = current_entry
    if not os.path.isdir(default_path):
        default_path = os.path.dirname(default_path)
    return os.path.expanduser(default_path or "~")
get_filechooser_dialog(self)

Return an instance of a FileChooserNative configured for this widget

Source code in lutris/gui/widgets/common.py
def get_filechooser_dialog(self):
    """Return an instance of a FileChooserNative configured for this widget"""
    dialog = Gtk.FileChooserNative.new(self.title, None, self.action, _("_OK"), _("_Cancel"))
    dialog.set_create_folders(True)
    dialog.set_current_folder(self.get_default_folder())
    dialog.connect("response", self.on_select_file, dialog)
    return dialog
get_filename(self)

Deprecated

Source code in lutris/gui/widgets/common.py
def get_filename(self):
    """Deprecated"""
    logger.warning("Just use get_text")
    return self.get_text()
get_text(self)

Return the entry's text

Source code in lutris/gui/widgets/common.py
def get_text(self):
    """Return the entry's text"""
    return self.entry.get_text()
on_browse_clicked(self, _widget)

Browse button click callback

Source code in lutris/gui/widgets/common.py
def on_browse_clicked(self, _widget):
    """Browse button click callback"""
    file_chooser_dialog = self.get_filechooser_dialog()
    file_chooser_dialog.show()
on_entry_changed(self, widget)

Entry changed callback

Source code in lutris/gui/widgets/common.py
def on_entry_changed(self, widget):
    """Entry changed callback"""
    self.clear_warnings()
    path = widget.get_text()
    if not path:
        return
    path = os.path.expanduser(path)
    self.update_completion(path)
    self.path = path
    if self.warn_if_ntfs and LINUX_SYSTEM.get_fs_type_for_path(path) == "ntfs":
        ntfs_box = Gtk.Box(spacing=6, visible=True)
        warning_image = Gtk.Image(visible=True)
        warning_image.set_from_pixbuf(get_stock_icon("dialog-warning", 32))
        ntfs_box.add(warning_image)
        ntfs_label = Gtk.Label(visible=True)
        ntfs_label.set_markup(_(
            "<b>Warning!</b> The selected path is located on a drive formatted by Windows.\n"
            "Games and programs installed on Windows drives usually <b>don't work</b>."
        ))
        ntfs_box.add(ntfs_label)
        self.pack_end(ntfs_box, False, False, 10)
    if self.warn_if_non_empty and os.path.exists(path) and os.listdir(path):
        non_empty_label = Gtk.Label(visible=True)
        non_empty_label.set_markup(_(
            "<b>Warning!</b> The selected path "
            "contains files. Installation might not work properly."
        ))
        self.pack_end(non_empty_label, False, False, 10)
    parent = system.get_existing_parent(path)
    if parent is not None and not os.access(parent, os.W_OK):
        non_writable_destination_label = Gtk.Label(visible=True)
        non_writable_destination_label.set_markup(_(
            "<b>Warning</b> The destination folder "
            "is not writable by the current user."
        ))
        self.pack_end(non_writable_destination_label, False, False, 10)
on_select_file(self, dialog, response, _dialog)

FileChooserDialog response callback

Source code in lutris/gui/widgets/common.py
def on_select_file(self, dialog, response, _dialog):
    """FileChooserDialog response callback"""
    if response == Gtk.ResponseType.ACCEPT:
        target_path = dialog.get_filename()
        if target_path:
            dialog.set_current_folder(target_path)
            self.entry.set_text(system.reverse_expanduser(target_path))
    dialog.destroy()
update_completion(self, current_path)

Update the auto-completion widget with the current path

Source code in lutris/gui/widgets/common.py
def update_completion(self, current_path):
    """Update the auto-completion widget with the current path"""
    self.path_completion.clear()

    if not os.path.exists(current_path):
        current_path, filefilter = os.path.split(current_path)
    else:
        filefilter = None

    if os.path.isdir(current_path):
        index = 0
        for filename in sorted(os.listdir(current_path)):
            if filename.startswith("."):
                continue
            if filefilter is not None and not filename.startswith(filefilter):
                continue
            self.path_completion.append([os.path.join(current_path, filename)])
            index += 1
            if index > self.max_completion_items:
                break
InstallerLabel (Label)

Label for installer window

Source code in lutris/gui/widgets/common.py
class InstallerLabel(Gtk.Label):
    """Label for installer window"""

    def __init__(self, message=None):
        super().__init__(label=message)
        self.set_max_width_chars(80)
        self.set_property("wrap", True)
        self.set_use_markup(True)
        self.set_selectable(True)
        self.set_alignment(0.5, 0)
__init__(self, message=None) special
Source code in lutris/gui/widgets/common.py
def __init__(self, message=None):
    super().__init__(label=message)
    self.set_max_width_chars(80)
    self.set_property("wrap", True)
    self.set_use_markup(True)
    self.set_selectable(True)
    self.set_alignment(0.5, 0)
Label (Label)

Standardised label for config vboxes.

Source code in lutris/gui/widgets/common.py
class Label(Gtk.Label):
    """Standardised label for config vboxes."""

    def __init__(self, message=None):
        """Custom init of label."""
        super().__init__(label=message)
        self.set_line_wrap(True)
        self.set_max_width_chars(22)
        self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
        self.set_size_request(230, -1)
        self.set_alignment(0, 0.5)
        self.set_justify(Gtk.Justification.LEFT)
__init__(self, message=None) special

Custom init of label.

Source code in lutris/gui/widgets/common.py
def __init__(self, message=None):
    """Custom init of label."""
    super().__init__(label=message)
    self.set_line_wrap(True)
    self.set_max_width_chars(22)
    self.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
    self.set_size_request(230, -1)
    self.set_alignment(0, 0.5)
    self.set_justify(Gtk.Justification.LEFT)
NumberEntry (Entry, Editable)
Source code in lutris/gui/widgets/common.py
class NumberEntry(Gtk.Entry, Gtk.Editable):

    def do_insert_text(self, new_text, length, position):
        """Filter inserted characters to only accept numbers"""
        new_text = "".join([c for c in new_text if c.isnumeric()])
        if new_text:
            self.get_buffer().insert_text(position, new_text, length)
            return position + length
        return position
do_insert_text(self, new_text, length, position)

Filter inserted characters to only accept numbers

Source code in lutris/gui/widgets/common.py
def do_insert_text(self, new_text, length, position):
    """Filter inserted characters to only accept numbers"""
    new_text = "".join([c for c in new_text if c.isnumeric()])
    if new_text:
        self.get_buffer().insert_text(position, new_text, length)
        return position + length
    return position
SlugEntry (Entry, Editable)
Source code in lutris/gui/widgets/common.py
class SlugEntry(Gtk.Entry, Gtk.Editable):

    def do_insert_text(self, new_text, length, position):
        """Filter inserted characters to only accept alphanumeric and dashes"""
        new_text = "".join([c for c in new_text if c.isalnum() or c == "-"]).lower()
        length = len(new_text)
        self.get_buffer().insert_text(position, new_text, length)
        return position + length
do_insert_text(self, new_text, length, position)

Filter inserted characters to only accept alphanumeric and dashes

Source code in lutris/gui/widgets/common.py
def do_insert_text(self, new_text, length, position):
    """Filter inserted characters to only accept alphanumeric and dashes"""
    new_text = "".join([c for c in new_text if c.isalnum() or c == "-"]).lower()
    length = len(new_text)
    self.get_buffer().insert_text(position, new_text, length)
    return position + length
VBox (Box)
Source code in lutris/gui/widgets/common.py
class VBox(Gtk.Box):
    def __init__(self, **kwargs):
        super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)
__init__(self, **kwargs) special
Source code in lutris/gui/widgets/common.py
def __init__(self, **kwargs):
    super().__init__(orientation=Gtk.Orientation.VERTICAL, margin_top=18, **kwargs)

contextual_menu

ContextualMenu (Menu)
Source code in lutris/gui/widgets/contextual_menu.py
class ContextualMenu(Gtk.Menu):
    def __init__(self, main_entries):
        super().__init__()
        self.main_entries = main_entries

    def add_menuitem(self, entry):
        """Add a menu item to the current menu

        Params:
            entry (tuple): tuple containing name, label and callback

        Returns:
            Gtk.MenuItem
        """
        name, label, callback = entry
        action = Gtk.Action(name=name, label=label)
        action.connect("activate", callback)

        menu_item = action.create_menu_item()
        menu_item.action_id = name
        self.append(menu_item)
        return menu_item

    def get_runner_entries(self, game):
        if not game:
            return None
        try:
            runner = runners.import_runner(game.runner_name)(game.config)
        except runners.InvalidRunner:
            return None
        return runner.context_menu_entries

    def popup(self, event, game_actions, game=None, service=None):
        for item in self.get_children():
            self.remove(item)

        for entry in self.main_entries:
            self.add_menuitem(entry)

        if game_actions.game.runner_name and game_actions.game.is_installed:
            runner_entries = self.get_runner_entries(game)
            if runner_entries:
                self.append(Gtk.SeparatorMenuItem())
                for entry in runner_entries:
                    self.add_menuitem(entry)
        self.show_all()

        displayed = game_actions.get_displayed_entries()
        for menuitem in self.get_children():
            if not isinstance(menuitem, Gtk.ImageMenuItem):
                continue
            menuitem.set_visible(displayed.get(menuitem.action_id, True))

        super().popup_at_pointer(event)
__init__(self, main_entries) special
Source code in lutris/gui/widgets/contextual_menu.py
def __init__(self, main_entries):
    super().__init__()
    self.main_entries = main_entries
add_menuitem(self, entry)

Add a menu item to the current menu

Parameters:

Name Type Description Default
entry tuple

tuple containing name, label and callback

required

Returns:

Type Description

Gtk.MenuItem

Source code in lutris/gui/widgets/contextual_menu.py
def add_menuitem(self, entry):
    """Add a menu item to the current menu

    Params:
        entry (tuple): tuple containing name, label and callback

    Returns:
        Gtk.MenuItem
    """
    name, label, callback = entry
    action = Gtk.Action(name=name, label=label)
    action.connect("activate", callback)

    menu_item = action.create_menu_item()
    menu_item.action_id = name
    self.append(menu_item)
    return menu_item
get_runner_entries(self, game)
Source code in lutris/gui/widgets/contextual_menu.py
def get_runner_entries(self, game):
    if not game:
        return None
    try:
        runner = runners.import_runner(game.runner_name)(game.config)
    except runners.InvalidRunner:
        return None
    return runner.context_menu_entries
popup(self, event, game_actions, game=None, service=None)

popup(self, parent_menu_shell:Gtk.Widget=None, parent_menu_item:Gtk.Widget=None, func:Gtk.MenuPositionFunc=None, data=None, button:int, activate_time:int)

Source code in lutris/gui/widgets/contextual_menu.py
def popup(self, event, game_actions, game=None, service=None):
    for item in self.get_children():
        self.remove(item)

    for entry in self.main_entries:
        self.add_menuitem(entry)

    if game_actions.game.runner_name and game_actions.game.is_installed:
        runner_entries = self.get_runner_entries(game)
        if runner_entries:
            self.append(Gtk.SeparatorMenuItem())
            for entry in runner_entries:
                self.add_menuitem(entry)
    self.show_all()

    displayed = game_actions.get_displayed_entries()
    for menuitem in self.get_children():
        if not isinstance(menuitem, Gtk.ImageMenuItem):
            continue
        menuitem.set_visible(displayed.get(menuitem.action_id, True))

    super().popup_at_pointer(event)

download_progress_box

DownloadProgressBox (Box)

Progress bar used to monitor a file download.

Source code in lutris/gui/widgets/download_progress_box.py
class DownloadProgressBox(Gtk.Box):

    """Progress bar used to monitor a file download."""

    __gsignals__ = {
        "complete": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
        "cancel": (GObject.SignalFlags.RUN_LAST, None, ()),
        "error": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, )),
    }

    def __init__(self, params, cancelable=True, downloader=None):
        super().__init__(orientation=Gtk.Orientation.VERTICAL)

        self.downloader = downloader
        self.is_complete = False
        self.url = params.get("url")
        self.dest = params.get("dest")
        self.referer = params.get("referer")

        self.main_label = Gtk.Label(self.get_title())
        self.main_label.set_alignment(0, 0)
        self.main_label.set_property("wrap", True)
        self.main_label.set_margin_bottom(10)
        # self.main_label.set_max_width_chars(70)
        self.main_label.set_selectable(True)
        self.main_label.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
        self.pack_start(self.main_label, True, True, 0)

        progress_box = Gtk.Box()

        self.progressbar = Gtk.ProgressBar()
        self.progressbar.set_margin_top(5)
        self.progressbar.set_margin_bottom(5)
        self.progressbar.set_margin_right(10)
        progress_box.pack_start(self.progressbar, True, True, 0)

        self.cancel_button = Gtk.Button.new_with_mnemonic(_("_Cancel"))
        self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_cancel_clicked)
        if not cancelable:
            self.cancel_button.set_sensitive(False)
        progress_box.pack_end(self.cancel_button, False, False, 0)

        self.pack_start(progress_box, False, False, 0)

        self.progress_label = Gtk.Label()
        self.progress_label.set_alignment(0, 0)
        self.pack_start(self.progress_label, True, True, 0)

        self.show_all()
        self.cancel_button.hide()

    def get_title(self):
        """Return the main label text for the widget"""
        parsed = urlparse(self.url)
        return "%s%s" % (parsed.netloc, parsed.path)

    def start(self):
        """Start downloading a file."""
        if not self.downloader:
            try:
                self.downloader = Downloader(self.url, self.dest, referer=self.referer, overwrite=True)
            except RuntimeError as ex:
                from lutris.gui.dialogs import ErrorDialog

                ErrorDialog(ex.args[0])
                self.emit("cancel")
                return None

        timer_id = GLib.timeout_add(500, self._progress)
        self.cancel_button.show()
        self.cancel_button.set_sensitive(True)
        if not self.downloader.state == self.downloader.DOWNLOADING:
            self.downloader.start()
        return timer_id

    def set_retry_button(self):
        """Transform the cancel button into a retry button"""
        self.cancel_button.set_label(_("Retry"))
        self.cancel_button.disconnect(self.cancel_cb_id)
        self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_retry_clicked)
        self.cancel_button.set_sensitive(True)

    def on_retry_clicked(self, button):
        logger.debug("Retrying download")
        button.set_label(_("Cancel"))
        button.disconnect(self.cancel_cb_id)
        self.cancel_cb_id = button.connect("clicked", self.on_cancel_clicked)
        self.downloader.reset()
        self.start()

    def on_cancel_clicked(self, _widget=None):
        """Cancel the current download."""
        logger.debug("Download cancel requested")
        if self.downloader:
            self.downloader.cancel()
        self.cancel_button.set_sensitive(False)
        self.emit("cancel")

    def _progress(self):
        """Show download progress."""
        progress = min(self.downloader.check_progress(), 1)
        if self.downloader.state in [self.downloader.CANCELLED, self.downloader.ERROR]:
            self.progressbar.set_fraction(0)
            if self.downloader.state == self.downloader.CANCELLED:
                self._set_text(_("Download interrupted"))
                self.emit("cancel")
            else:
                self._set_text(str(self.downloader.error)[:80])
            return False
        self.progressbar.set_fraction(progress)
        megabytes = 1024 * 1024
        progress_text = _(
            "{downloaded:0.2f} / {size:0.2f}MB ({speed:0.2f}MB/s), {time} remaining"
        ).format(
            downloaded=float(self.downloader.downloaded_size) / megabytes,
            size=float(self.downloader.full_size) / megabytes,
            speed=float(self.downloader.average_speed) / megabytes,
            time=self.downloader.time_left,
        )
        self._set_text(progress_text)
        if self.downloader.state == self.downloader.COMPLETED:
            self.cancel_button.set_sensitive(False)
            self.is_complete = True
            self.emit("complete", {})
            return False
        return True

    def _set_text(self, text):
        markup = "<span size='10000'>{}</span>".format(gtk_safe(text))
        self.progress_label.set_markup(markup)
__init__(self, params, cancelable=True, downloader=None) special
Source code in lutris/gui/widgets/download_progress_box.py
def __init__(self, params, cancelable=True, downloader=None):
    super().__init__(orientation=Gtk.Orientation.VERTICAL)

    self.downloader = downloader
    self.is_complete = False
    self.url = params.get("url")
    self.dest = params.get("dest")
    self.referer = params.get("referer")

    self.main_label = Gtk.Label(self.get_title())
    self.main_label.set_alignment(0, 0)
    self.main_label.set_property("wrap", True)
    self.main_label.set_margin_bottom(10)
    # self.main_label.set_max_width_chars(70)
    self.main_label.set_selectable(True)
    self.main_label.set_property("ellipsize", Pango.EllipsizeMode.MIDDLE)
    self.pack_start(self.main_label, True, True, 0)

    progress_box = Gtk.Box()

    self.progressbar = Gtk.ProgressBar()
    self.progressbar.set_margin_top(5)
    self.progressbar.set_margin_bottom(5)
    self.progressbar.set_margin_right(10)
    progress_box.pack_start(self.progressbar, True, True, 0)

    self.cancel_button = Gtk.Button.new_with_mnemonic(_("_Cancel"))
    self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_cancel_clicked)
    if not cancelable:
        self.cancel_button.set_sensitive(False)
    progress_box.pack_end(self.cancel_button, False, False, 0)

    self.pack_start(progress_box, False, False, 0)

    self.progress_label = Gtk.Label()
    self.progress_label.set_alignment(0, 0)
    self.pack_start(self.progress_label, True, True, 0)

    self.show_all()
    self.cancel_button.hide()
get_title(self)

Return the main label text for the widget

Source code in lutris/gui/widgets/download_progress_box.py
def get_title(self):
    """Return the main label text for the widget"""
    parsed = urlparse(self.url)
    return "%s%s" % (parsed.netloc, parsed.path)
on_cancel_clicked(self, _widget=None)

Cancel the current download.

Source code in lutris/gui/widgets/download_progress_box.py
def on_cancel_clicked(self, _widget=None):
    """Cancel the current download."""
    logger.debug("Download cancel requested")
    if self.downloader:
        self.downloader.cancel()
    self.cancel_button.set_sensitive(False)
    self.emit("cancel")
on_retry_clicked(self, button)
Source code in lutris/gui/widgets/download_progress_box.py
def on_retry_clicked(self, button):
    logger.debug("Retrying download")
    button.set_label(_("Cancel"))
    button.disconnect(self.cancel_cb_id)
    self.cancel_cb_id = button.connect("clicked", self.on_cancel_clicked)
    self.downloader.reset()
    self.start()
set_retry_button(self)

Transform the cancel button into a retry button

Source code in lutris/gui/widgets/download_progress_box.py
def set_retry_button(self):
    """Transform the cancel button into a retry button"""
    self.cancel_button.set_label(_("Retry"))
    self.cancel_button.disconnect(self.cancel_cb_id)
    self.cancel_cb_id = self.cancel_button.connect("clicked", self.on_retry_clicked)
    self.cancel_button.set_sensitive(True)
start(self)

Start downloading a file.

Source code in lutris/gui/widgets/download_progress_box.py
def start(self):
    """Start downloading a file."""
    if not self.downloader:
        try:
            self.downloader = Downloader(self.url, self.dest, referer=self.referer, overwrite=True)
        except RuntimeError as ex:
            from lutris.gui.dialogs import ErrorDialog

            ErrorDialog(ex.args[0])
            self.emit("cancel")
            return None

    timer_id = GLib.timeout_add(500, self._progress)
    self.cancel_button.show()
    self.cancel_button.set_sensitive(True)
    if not self.downloader.state == self.downloader.DOWNLOADING:
        self.downloader.start()
    return timer_id

game_bar

GameBar (Box)
Source code in lutris/gui/widgets/game_bar.py
class GameBar(Gtk.Box):
    def __init__(self, db_game, game_actions, application):
        """Create the game bar with a database row"""
        super().__init__(orientation=Gtk.Orientation.VERTICAL, visible=True,
                         margin_top=12,
                         margin_left=12,
                         margin_bottom=12,
                         margin_right=12,
                         spacing=6)
        self.game_start_hook_id = GObject.add_emission_hook(Game, "game-start", self.on_game_state_changed)
        self.game_started_hook_id = GObject.add_emission_hook(Game, "game-started", self.on_game_state_changed)
        self.game_stopped_hook_id = GObject.add_emission_hook(Game, "game-stopped", self.on_game_state_changed)
        self.game_updated_hook_id = GObject.add_emission_hook(Game, "game-updated", self.on_game_state_changed)
        self.game_removed_hook_id = GObject.add_emission_hook(Game, "game-removed", self.on_game_state_changed)
        self.game_installed_hook_id = GObject.add_emission_hook(Game, "game-installed", self.on_game_state_changed)
        self.connect("destroy", self.on_destroy)

        self.set_margin_bottom(12)
        self.game_actions = game_actions
        self.db_game = db_game
        self.service = None
        if db_game.get("service"):
            try:
                self.service = services.SERVICES[db_game["service"]]()
            except KeyError:
                pass

        game_id = None
        if "service_id" in db_game:
            self.appid = db_game["service_id"]
            game_id = db_game["id"]
        elif self.service:
            self.appid = db_game["appid"]
            if self.service.id == "lutris":
                game = get_game_by_field(self.appid, field="slug")
            else:
                game = get_game_for_service(self.service.id, self.appid)
            if game:
                game_id = game["id"]
        if game_id:
            self.game = application.get_game_by_id(game_id) or Game(game_id)
        else:
            self.game = Game()
            self.game.name = db_game["name"]
            self.game.slug = db_game["slug"]
            self.game.appid = self.appid
            self.game.service = self.service.id if self.service else None
        game_actions.set_game(self.game)
        self.update_view()

    def on_destroy(self, widget):
        GObject.remove_emission_hook(Game, "game-start", self.game_start_hook_id)
        GObject.remove_emission_hook(Game, "game-started", self.game_started_hook_id)
        GObject.remove_emission_hook(Game, "game-stopped", self.game_stopped_hook_id)
        GObject.remove_emission_hook(Game, "game-updated", self.game_updated_hook_id)
        GObject.remove_emission_hook(Game, "game-removed", self.game_removed_hook_id)
        GObject.remove_emission_hook(Game, "game-installed", self.game_installed_hook_id)
        return True

    def clear_view(self):
        """Clears all widgets from the container"""
        for child in self.get_children():
            child.destroy()

    def update_view(self):
        """Populate the view with widgets"""
        game_label = self.get_game_name_label()
        game_label.set_halign(Gtk.Align.START)
        self.pack_start(game_label, False, False, 0)

        hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, spacing=6)
        self.pack_start(hbox, False, False, 0)

        self.play_button = self.get_play_button()
        hbox.pack_start(self.play_button, False, False, 0)

        if self.game.is_installed:
            hbox.pack_start(self.get_runner_button(), False, False, 0)
            hbox.pack_start(self.get_platform_label(), False, False, 0)
        if self.game.lastplayed:
            hbox.pack_start(self.get_last_played_label(), False, False, 0)
        if self.game.playtime:
            hbox.pack_start(self.get_playtime_label(), False, False, 0)
        hbox.show_all()

    def get_popover(self, buttons, parent):
        """Return the popover widget containing a list of link buttons"""
        if not buttons:
            return None
        popover = Gtk.Popover()
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)

        for action in buttons:
            vbox.pack_end(buttons[action], False, False, 1)
        popover.add(vbox)
        popover.set_position(Gtk.PositionType.TOP)
        popover.set_constrain_to(Gtk.PopoverConstraint.NONE)
        popover.set_relative_to(parent)
        return popover

    def get_game_name_label(self):
        """Return the label with the game's title"""
        title_label = Gtk.Label(visible=True)
        title_label.set_ellipsize(Pango.EllipsizeMode.END)
        title_label.set_markup("<span font_desc='16'><b>%s</b></span>" % gtk_safe(self.game.name))
        return title_label

    def get_runner_button(self):
        icon_name = self.game.runner.name + "-symbolic"
        runner_icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
        runner_icon.show()
        box = Gtk.HBox(visible=True)
        runner_button = Gtk.Button(visible=True)
        popover = self.get_popover(self.get_runner_buttons(), runner_button)
        if popover:
            runner_button.set_image(runner_icon)
            popover_button = Gtk.MenuButton(visible=True)
            popover_button.set_size_request(32, 32)
            popover_button.props.direction = Gtk.ArrowType.UP
            popover_button.set_popover(popover)
            runner_button.connect("clicked", lambda _x: popover_button.emit("clicked"))
            box.add(runner_button)
            box.add(popover_button)
            style_context = box.get_style_context()
            style_context.add_class("linked")
        else:
            runner_icon.set_margin_left(49)
            runner_icon.set_margin_right(6)
            box.add(runner_icon)
        return box

    def get_platform_label(self):
        platform_label = Gtk.Label(visible=True)
        platform_label.set_size_request(120, -1)
        platform_label.set_alignment(0, 0.5)
        platform = gtk_safe(self.game.platform)
        platform_label.set_tooltip_markup(platform)
        platform_label.set_markup(_("Platform:\n<b>%s</b>") % platform)
        platform_label.set_property("ellipsize", Pango.EllipsizeMode.END)
        return platform_label

    def get_playtime_label(self):
        """Return the label containing the playtime info"""
        playtime_label = Gtk.Label(visible=True)
        playtime_label.set_size_request(120, -1)
        playtime_label.set_alignment(0, 0.5)
        playtime_label.set_markup(_("Time played:\n<b>%s</b>") % self.game.formatted_playtime)
        return playtime_label

    def get_last_played_label(self):
        """Return the label containing the last played info"""
        last_played_label = Gtk.Label(visible=True)
        last_played_label.set_size_request(120, -1)
        last_played_label.set_alignment(0, 0.5)
        lastplayed = datetime.fromtimestamp(self.game.lastplayed)
        last_played_label.set_markup(_("Last played:\n<b>%s</b>") % lastplayed.strftime("%b %-d %Y"))
        return last_played_label

    def get_popover_button(self):
        """Return the popover button+menu for the Play button"""
        popover_button = Gtk.MenuButton(visible=True)
        popover_button.set_size_request(32, 32)
        popover_button.props.direction = Gtk.ArrowType.UP

        return popover_button

    def get_popover_box(self):
        """Return a container for a button + a popover button attached to it"""
        box = Gtk.HBox(visible=True)
        style_context = box.get_style_context()
        style_context.add_class("linked")
        return box

    def get_locate_installed_game_button(self):
        """Return a button to locate an existing install"""
        button = get_link_button("Locate installed game")
        button.show()
        button.connect("clicked", self.game_actions.on_locate_installed_game, self.game)
        return {"locate": button}

    def get_play_button(self):
        """Return the widget for install/play/stop and game config"""
        button = Gtk.Button(visible=True)
        button.set_size_request(120, 32)
        box = self.get_popover_box()
        popover_button = self.get_popover_button()
        if self.game.is_installed:
            if self.game.state == self.game.STATE_STOPPED:
                button.set_label(_("Play"))
                button.connect("clicked", self.game_actions.on_game_launch)
            elif self.game.state == self.game.STATE_LAUNCHING:
                button.set_label(_("Launching"))
                button.set_sensitive(False)
            else:
                button.set_label(_("Stop"))
                button.connect("clicked", self.game_actions.on_game_stop)
        else:
            button.set_label(_("Install"))
            button.connect("clicked", self.game_actions.on_install_clicked)
            if self.service:
                if self.service.local:
                    # Local services don't show an install dialog, they can be launched directly
                    button.set_label(_("Play"))
                if self.service.drm_free:
                    button.set_size_request(84, 32)
                    box.add(button)
                    popover = self.get_popover(self.get_locate_installed_game_button(), popover_button)
                    popover_button.set_popover(popover)
                    box.add(popover_button)
                    return box
                return button
        button.set_size_request(84, 32)
        box.add(button)
        popover = self.get_popover(self.get_game_buttons(), popover_button)
        popover_button.set_popover(popover)
        box.add(popover_button)
        return box

    def get_game_buttons(self):
        """Return a dictionary of buttons to use in the panel"""
        displayed = self.game_actions.get_displayed_entries()
        buttons = {}
        for action in self.game_actions.get_game_actions():
            action_id, label, callback = action
            if action_id in ("play", "stop", "install"):
                continue
            button = get_link_button(label)
            if displayed.get(action_id):
                button.show()
            else:
                button.hide()
            buttons[action_id] = button
            button.connect("clicked", self.on_link_button_clicked, callback)
        return buttons

    def get_runner_buttons(self):
        buttons = {}
        if self.game.runner_name and self.game.is_installed:
            runner = runners.import_runner(self.game.runner_name)(self.game.config)
            for entry in runner.context_menu_entries:
                name, label, callback = entry
                button = get_link_button(label)
                button.show()
                button.connect("clicked", self.on_link_button_clicked, callback)
                buttons[name] = button
        return buttons

    def on_link_button_clicked(self, button, callback):
        """Callback for link buttons. Closes the popover then runs the actual action"""
        popover = button.get_parent().get_parent()
        popover.popdown()
        callback(button)

    def on_install_clicked(self, button):
        """Handler for installing service games"""
        self.service.install(self.db_game)

    def on_game_state_changed(self, game):
        """Handler called when the game has changed state"""
        if (
            game.id == self.game.id
            or (self.appid and game.appid == self.appid)
        ):
            self.game = game
        else:
            return True
        self.clear_view()
        self.update_view()
        return True
__init__(self, db_game, game_actions, application) special

Create the game bar with a database row

Source code in lutris/gui/widgets/game_bar.py
def __init__(self, db_game, game_actions, application):
    """Create the game bar with a database row"""
    super().__init__(orientation=Gtk.Orientation.VERTICAL, visible=True,
                     margin_top=12,
                     margin_left=12,
                     margin_bottom=12,
                     margin_right=12,
                     spacing=6)
    self.game_start_hook_id = GObject.add_emission_hook(Game, "game-start", self.on_game_state_changed)
    self.game_started_hook_id = GObject.add_emission_hook(Game, "game-started", self.on_game_state_changed)
    self.game_stopped_hook_id = GObject.add_emission_hook(Game, "game-stopped", self.on_game_state_changed)
    self.game_updated_hook_id = GObject.add_emission_hook(Game, "game-updated", self.on_game_state_changed)
    self.game_removed_hook_id = GObject.add_emission_hook(Game, "game-removed", self.on_game_state_changed)
    self.game_installed_hook_id = GObject.add_emission_hook(Game, "game-installed", self.on_game_state_changed)
    self.connect("destroy", self.on_destroy)

    self.set_margin_bottom(12)
    self.game_actions = game_actions
    self.db_game = db_game
    self.service = None
    if db_game.get("service"):
        try:
            self.service = services.SERVICES[db_game["service"]]()
        except KeyError:
            pass

    game_id = None
    if "service_id" in db_game:
        self.appid = db_game["service_id"]
        game_id = db_game["id"]
    elif self.service:
        self.appid = db_game["appid"]
        if self.service.id == "lutris":
            game = get_game_by_field(self.appid, field="slug")
        else:
            game = get_game_for_service(self.service.id, self.appid)
        if game:
            game_id = game["id"]
    if game_id:
        self.game = application.get_game_by_id(game_id) or Game(game_id)
    else:
        self.game = Game()
        self.game.name = db_game["name"]
        self.game.slug = db_game["slug"]
        self.game.appid = self.appid
        self.game.service = self.service.id if self.service else None
    game_actions.set_game(self.game)
    self.update_view()
clear_view(self)

Clears all widgets from the container

Source code in lutris/gui/widgets/game_bar.py
def clear_view(self):
    """Clears all widgets from the container"""
    for child in self.get_children():
        child.destroy()
get_game_buttons(self)

Return a dictionary of buttons to use in the panel

Source code in lutris/gui/widgets/game_bar.py
def get_game_buttons(self):
    """Return a dictionary of buttons to use in the panel"""
    displayed = self.game_actions.get_displayed_entries()
    buttons = {}
    for action in self.game_actions.get_game_actions():
        action_id, label, callback = action
        if action_id in ("play", "stop", "install"):
            continue
        button = get_link_button(label)
        if displayed.get(action_id):
            button.show()
        else:
            button.hide()
        buttons[action_id] = button
        button.connect("clicked", self.on_link_button_clicked, callback)
    return buttons
get_game_name_label(self)

Return the label with the game's title

Source code in lutris/gui/widgets/game_bar.py
def get_game_name_label(self):
    """Return the label with the game's title"""
    title_label = Gtk.Label(visible=True)
    title_label.set_ellipsize(Pango.EllipsizeMode.END)
    title_label.set_markup("<span font_desc='16'><b>%s</b></span>" % gtk_safe(self.game.name))
    return title_label
get_last_played_label(self)

Return the label containing the last played info

Source code in lutris/gui/widgets/game_bar.py
def get_last_played_label(self):
    """Return the label containing the last played info"""
    last_played_label = Gtk.Label(visible=True)
    last_played_label.set_size_request(120, -1)
    last_played_label.set_alignment(0, 0.5)
    lastplayed = datetime.fromtimestamp(self.game.lastplayed)
    last_played_label.set_markup(_("Last played:\n<b>%s</b>") % lastplayed.strftime("%b %-d %Y"))
    return last_played_label
get_locate_installed_game_button(self)

Return a button to locate an existing install

Source code in lutris/gui/widgets/game_bar.py
def get_locate_installed_game_button(self):
    """Return a button to locate an existing install"""
    button = get_link_button("Locate installed game")
    button.show()
    button.connect("clicked", self.game_actions.on_locate_installed_game, self.game)
    return {"locate": button}
get_platform_label(self)
Source code in lutris/gui/widgets/game_bar.py
def get_platform_label(self):
    platform_label = Gtk.Label(visible=True)
    platform_label.set_size_request(120, -1)
    platform_label.set_alignment(0, 0.5)
    platform = gtk_safe(self.game.platform)
    platform_label.set_tooltip_markup(platform)
    platform_label.set_markup(_("Platform:\n<b>%s</b>") % platform)
    platform_label.set_property("ellipsize", Pango.EllipsizeMode.END)
    return platform_label
get_play_button(self)

Return the widget for install/play/stop and game config

Source code in lutris/gui/widgets/game_bar.py
def get_play_button(self):
    """Return the widget for install/play/stop and game config"""
    button = Gtk.Button(visible=True)
    button.set_size_request(120, 32)
    box = self.get_popover_box()
    popover_button = self.get_popover_button()
    if self.game.is_installed:
        if self.game.state == self.game.STATE_STOPPED:
            button.set_label(_("Play"))
            button.connect("clicked", self.game_actions.on_game_launch)
        elif self.game.state == self.game.STATE_LAUNCHING:
            button.set_label(_("Launching"))
            button.set_sensitive(False)
        else:
            button.set_label(_("Stop"))
            button.connect("clicked", self.game_actions.on_game_stop)
    else:
        button.set_label(_("Install"))
        button.connect("clicked", self.game_actions.on_install_clicked)
        if self.service:
            if self.service.local:
                # Local services don't show an install dialog, they can be launched directly
                button.set_label(_("Play"))
            if self.service.drm_free:
                button.set_size_request(84, 32)
                box.add(button)
                popover = self.get_popover(self.get_locate_installed_game_button(), popover_button)
                popover_button.set_popover(popover)
                box.add(popover_button)
                return box
            return button
    button.set_size_request(84, 32)
    box.add(button)
    popover = self.get_popover(self.get_game_buttons(), popover_button)
    popover_button.set_popover(popover)
    box.add(popover_button)
    return box
get_playtime_label(self)

Return the label containing the playtime info

Source code in lutris/gui/widgets/game_bar.py
def get_playtime_label(self):
    """Return the label containing the playtime info"""
    playtime_label = Gtk.Label(visible=True)
    playtime_label.set_size_request(120, -1)
    playtime_label.set_alignment(0, 0.5)
    playtime_label.set_markup(_("Time played:\n<b>%s</b>") % self.game.formatted_playtime)
    return playtime_label
get_popover(self, buttons, parent)

Return the popover widget containing a list of link buttons

Source code in lutris/gui/widgets/game_bar.py
def get_popover(self, buttons, parent):
    """Return the popover widget containing a list of link buttons"""
    if not buttons:
        return None
    popover = Gtk.Popover()
    vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)

    for action in buttons:
        vbox.pack_end(buttons[action], False, False, 1)
    popover.add(vbox)
    popover.set_position(Gtk.PositionType.TOP)
    popover.set_constrain_to(Gtk.PopoverConstraint.NONE)
    popover.set_relative_to(parent)
    return popover
get_popover_box(self)

Return a container for a button + a popover button attached to it

Source code in lutris/gui/widgets/game_bar.py
def get_popover_box(self):
    """Return a container for a button + a popover button attached to it"""
    box = Gtk.HBox(visible=True)
    style_context = box.get_style_context()
    style_context.add_class("linked")
    return box
get_popover_button(self)

Return the popover button+menu for the Play button

Source code in lutris/gui/widgets/game_bar.py
def get_popover_button(self):
    """Return the popover button+menu for the Play button"""
    popover_button = Gtk.MenuButton(visible=True)
    popover_button.set_size_request(32, 32)
    popover_button.props.direction = Gtk.ArrowType.UP

    return popover_button
get_runner_button(self)
Source code in lutris/gui/widgets/game_bar.py
def get_runner_button(self):
    icon_name = self.game.runner.name + "-symbolic"
    runner_icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.MENU)
    runner_icon.show()
    box = Gtk.HBox(visible=True)
    runner_button = Gtk.Button(visible=True)
    popover = self.get_popover(self.get_runner_buttons(), runner_button)
    if popover:
        runner_button.set_image(runner_icon)
        popover_button = Gtk.MenuButton(visible=True)
        popover_button.set_size_request(32, 32)
        popover_button.props.direction = Gtk.ArrowType.UP
        popover_button.set_popover(popover)
        runner_button.connect("clicked", lambda _x: popover_button.emit("clicked"))
        box.add(runner_button)
        box.add(popover_button)
        style_context = box.get_style_context()
        style_context.add_class("linked")
    else:
        runner_icon.set_margin_left(49)
        runner_icon.set_margin_right(6)
        box.add(runner_icon)
    return box
get_runner_buttons(self)
Source code in lutris/gui/widgets/game_bar.py
def get_runner_buttons(self):
    buttons = {}
    if self.game.runner_name and self.game.is_installed:
        runner = runners.import_runner(self.game.runner_name)(self.game.config)
        for entry in runner.context_menu_entries:
            name, label, callback = entry
            button = get_link_button(label)
            button.show()
            button.connect("clicked", self.on_link_button_clicked, callback)
            buttons[name] = button
    return buttons
on_destroy(self, widget)
Source code in lutris/gui/widgets/game_bar.py
def on_destroy(self, widget):
    GObject.remove_emission_hook(Game, "game-start", self.game_start_hook_id)
    GObject.remove_emission_hook(Game, "game-started", self.game_started_hook_id)
    GObject.remove_emission_hook(Game, "game-stopped", self.game_stopped_hook_id)
    GObject.remove_emission_hook(Game, "game-updated", self.game_updated_hook_id)
    GObject.remove_emission_hook(Game, "game-removed", self.game_removed_hook_id)
    GObject.remove_emission_hook(Game, "game-installed", self.game_installed_hook_id)
    return True
on_game_state_changed(self, game)

Handler called when the game has changed state

Source code in lutris/gui/widgets/game_bar.py
def on_game_state_changed(self, game):
    """Handler called when the game has changed state"""
    if (
        game.id == self.game.id
        or (self.appid and game.appid == self.appid)
    ):
        self.game = game
    else:
        return True
    self.clear_view()
    self.update_view()
    return True
on_install_clicked(self, button)

Handler for installing service games

Source code in lutris/gui/widgets/game_bar.py
def on_install_clicked(self, button):
    """Handler for installing service games"""
    self.service.install(self.db_game)

Callback for link buttons. Closes the popover then runs the actual action

Source code in lutris/gui/widgets/game_bar.py
def on_link_button_clicked(self, button, callback):
    """Callback for link buttons. Closes the popover then runs the actual action"""
    popover = button.get_parent().get_parent()
    popover.popdown()
    callback(button)
update_view(self)

Populate the view with widgets

Source code in lutris/gui/widgets/game_bar.py
def update_view(self):
    """Populate the view with widgets"""
    game_label = self.get_game_name_label()
    game_label.set_halign(Gtk.Align.START)
    self.pack_start(game_label, False, False, 0)

    hbox = Gtk.Box(Gtk.Orientation.HORIZONTAL, spacing=6)
    self.pack_start(hbox, False, False, 0)

    self.play_button = self.get_play_button()
    hbox.pack_start(self.play_button, False, False, 0)

    if self.game.is_installed:
        hbox.pack_start(self.get_runner_button(), False, False, 0)
        hbox.pack_start(self.get_platform_label(), False, False, 0)
    if self.game.lastplayed:
        hbox.pack_start(self.get_last_played_label(), False, False, 0)
    if self.game.playtime:
        hbox.pack_start(self.get_playtime_label(), False, False, 0)
    hbox.show_all()

gi_composites

GtkTemplate implementation for PyGI

Blog post http://www.virtualroadside.com/blog/index.php/2015/05/24/gtk3-composite-widget-templates-for-python/

Github https://github.com/virtuald/pygi-composite-templates/blob/master/gi_composites.py

This should have landed in PyGObect and will be available without this shim in the future. See: https://gitlab.gnome.org/GNOME/pygobject/merge_requests/52

__all__ special
GtkTemplate

Use this class decorator to signify that a class is a composite widget which will receive widgets and connect to signals as defined in a UI template. You must call init_template to cause the widgets/signals to be initialized from the template::

@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):

    def __init__(self):
        super().__init__()
        self.init_template()

The 'ui' parameter can either be a file path or a GResource resource path::

@GtkTemplate(ui='/org/example/foo.ui')
class Foo(Gtk.Box):
    pass

To connect a signal to a method on your instance, do::

@GtkTemplate.Callback
def on_thing_happened(self, widget):
    pass

To create a child attribute that is retrieved from your template, add this to your class definition::

@GtkTemplate(ui='foo.ui')
class Foo(Gtk.Box):

    widget = GtkTemplate.Child()

Note: This is implemented as a class decorator, but if it were included with PyGI I suspect it might be better to do this in the GObject metaclass (or similar) so that init_template can be called automatically instead of forcing the user to do it.

.. note:: Due to limitations in PyGObject, you may not inherit from python objects that use the GtkTemplate decorator.

Source code in lutris/gui/widgets/gi_composites.py
class _GtkTemplate:

    """
        Use this class decorator to signify that a class is a composite
        widget which will receive widgets and connect to signals as
        defined in a UI template. You must call init_template to
        cause the widgets/signals to be initialized from the template::

            @GtkTemplate(ui='foo.ui')
            class Foo(Gtk.Box):

                def __init__(self):
                    super().__init__()
                    self.init_template()

        The 'ui' parameter can either be a file path or a GResource resource
        path::

            @GtkTemplate(ui='/org/example/foo.ui')
            class Foo(Gtk.Box):
                pass

        To connect a signal to a method on your instance, do::

            @GtkTemplate.Callback
            def on_thing_happened(self, widget):
                pass

        To create a child attribute that is retrieved from your template,
        add this to your class definition::

            @GtkTemplate(ui='foo.ui')
            class Foo(Gtk.Box):

                widget = GtkTemplate.Child()


        Note: This is implemented as a class decorator, but if it were
        included with PyGI I suspect it might be better to do this
        in the GObject metaclass (or similar) so that init_template
        can be called automatically instead of forcing the user to do it.

        .. note:: Due to limitations in PyGObject, you may not inherit from
                  python objects that use the GtkTemplate decorator.
    """

    __ui_path__ = None

    @staticmethod
    def Callback(f):
        """
            Decorator that designates a method to be attached to a signal from
            the template
        """
        f._gtk_callback = True  # pylint: disable=protected-access
        return f

    Child = _Child

    @staticmethod
    def set_ui_path(*path):
        """
            If using file paths instead of resources, call this *before*
            loading anything that uses GtkTemplate, or it will fail to load
            your template file

            :param path: one or more path elements, will be joined together
                         to create the final path

            TODO: Alternatively, could wait until first class instantiation
                  before registering templates? Would need a metaclass...
        """
        _GtkTemplate.__ui_path__ = abspath(join(*path))  # pylint: disable=no-value-for-parameter

    def __init__(self, ui):
        self.ui = ui

    def __call__(self, cls):

        if not issubclass(cls, Gtk.Widget):
            raise TypeError("Can only use @GtkTemplate on Widgets")

        # Nested templates don't work
        if hasattr(cls, "__gtemplate_methods__"):
            raise TypeError("Cannot nest template classes")

        # Load the template either from a resource path or a file
        # - Prefer the resource path first

        try:
            template_bytes = Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE)
        except GLib.GError:
            ui = self.ui
            if isinstance(ui, (list, tuple)):
                ui = join(ui)

            if _GtkTemplate.__ui_path__ is not None:
                ui = join(_GtkTemplate.__ui_path__, ui)

            with open(ui, "rb") as fp:
                template_bytes = GLib.Bytes.new(fp.read())

        _register_template(cls, template_bytes)
        return cls
__ui_path__ special
Child

Assign this to an attribute in your class definition and it will be replaced with a widget defined in the UI file when init_template is called

Source code in lutris/gui/widgets/gi_composites.py
class _Child:

    """
        Assign this to an attribute in your class definition and it will
        be replaced with a widget defined in the UI file when init_template
        is called
    """

    __slots__ = []

    @staticmethod
    def widgets(count):
        """
            Allows declaring multiple widgets with less typing::

                button    \
                label1    \
                label2    = GtkTemplate.Child.widgets(3)
        """
        return [_Child() for _ in range(count)]
__slots__ special
widgets(count) staticmethod

Allows declaring multiple widgets with less typing::

button                    label1                    label2    = GtkTemplate.Child.widgets(3)
Source code in lutris/gui/widgets/gi_composites.py
@staticmethod
def widgets(count):
    """
        Allows declaring multiple widgets with less typing::

            button    \
            label1    \
            label2    = GtkTemplate.Child.widgets(3)
    """
    return [_Child() for _ in range(count)]
Callback(f) staticmethod

Decorator that designates a method to be attached to a signal from the template

Source code in lutris/gui/widgets/gi_composites.py
@staticmethod
def Callback(f):
    """
        Decorator that designates a method to be attached to a signal from
        the template
    """
    f._gtk_callback = True  # pylint: disable=protected-access
    return f
__call__(self, cls) special
Source code in lutris/gui/widgets/gi_composites.py
def __call__(self, cls):

    if not issubclass(cls, Gtk.Widget):
        raise TypeError("Can only use @GtkTemplate on Widgets")

    # Nested templates don't work
    if hasattr(cls, "__gtemplate_methods__"):
        raise TypeError("Cannot nest template classes")

    # Load the template either from a resource path or a file
    # - Prefer the resource path first

    try:
        template_bytes = Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE)
    except GLib.GError:
        ui = self.ui
        if isinstance(ui, (list, tuple)):
            ui = join(ui)

        if _GtkTemplate.__ui_path__ is not None:
            ui = join(_GtkTemplate.__ui_path__, ui)

        with open(ui, "rb") as fp:
            template_bytes = GLib.Bytes.new(fp.read())

    _register_template(cls, template_bytes)
    return cls
__init__(self, ui) special
Source code in lutris/gui/widgets/gi_composites.py
def __init__(self, ui):
    self.ui = ui
set_ui_path(*path) staticmethod

If using file paths instead of resources, call this before loading anything that uses GtkTemplate, or it will fail to load your template file

:param path: one or more path elements, will be joined together to create the final path

Alternatively, could wait until first class instantiation

before registering templates? Would need a metaclass...

Source code in lutris/gui/widgets/gi_composites.py
@staticmethod
def set_ui_path(*path):
    """
        If using file paths instead of resources, call this *before*
        loading anything that uses GtkTemplate, or it will fail to load
        your template file

        :param path: one or more path elements, will be joined together
                     to create the final path

        TODO: Alternatively, could wait until first class instantiation
              before registering templates? Would need a metaclass...
    """
    _GtkTemplate.__ui_path__ = abspath(join(*path))  # pylint: disable=no-value-for-parameter
GtkTemplateWarning (UserWarning)
Source code in lutris/gui/widgets/gi_composites.py
class GtkTemplateWarning(UserWarning):
    pass

log_text_view

LogTextView (TextView)
Source code in lutris/gui/widgets/log_text_view.py
class LogTextView(Gtk.TextView):
    # pylint: disable=no-member

    def __init__(self, buffer=None, autoscroll=True):
        super().__init__(visible=True)

        if buffer:
            self.set_buffer(buffer)
        self.set_editable(False)
        self.set_cursor_visible(False)
        self.set_monospace(True)
        self.set_left_margin(10)
        self.scroll_max = 0
        self.set_wrap_mode(Gtk.WrapMode.CHAR)
        self.get_style_context().add_class("lutris-logview")

        self.mark = self.create_new_mark(self.props.buffer.get_start_iter())

        if autoscroll:
            self.connect("size-allocate", self.autoscroll)

    def autoscroll(self, *args):  # pylint: disable=unused-argument
        adj = self.get_vadjustment()
        if adj.get_value() == self.scroll_max or self.scroll_max == 0:
            adj.set_value(adj.get_upper() - adj.get_page_size())
            self.scroll_max = adj.get_value()
        else:
            self.scroll_max = adj.get_upper() - adj.get_page_size()

    def create_new_mark(self, buffer_iter):
        return self.props.buffer.create_mark(None, buffer_iter, True)

    def reset_search(self):
        self.props.buffer.delete_mark(self.mark)
        self.mark = self.create_new_mark(self.props.buffer.get_start_iter())
        self.props.buffer.place_cursor(self.props.buffer.get_iter_at_mark(self.mark))

    def find_first(self, searched_entry):
        self.reset_search()
        self.find_next(searched_entry)

    def find_next(self, searched_entry):
        buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
        next_occurence = buffer_iter.forward_search(
            searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
        )

        # Found nothing try from the beginning
        if next_occurence is None:
            next_occurence = self.props.buffer.get_start_iter(
            ).forward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)

        # Highlight if result
        if next_occurence is not None:
            self.highlight(next_occurence[0], next_occurence[1])
            self.props.buffer.delete_mark(self.mark)
            self.mark = self.create_new_mark(next_occurence[1])

    def find_previous(self, searched_entry):
        # First go to the beginning of searched_entry string
        buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
        buffer_iter.backward_chars(len(searched_entry.get_text()))

        previous_occurence = buffer_iter.backward_search(
            searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
        )

        # Found nothing ? Try from the end
        if previous_occurence is None:
            previous_occurence = self.props.buffer.get_end_iter(
            ).backward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)

        # Highlight if result
        if previous_occurence is not None:
            self.highlight(previous_occurence[0], previous_occurence[1])
            self.props.buffer.delete_mark(self.mark)
            self.mark = self.create_new_mark(previous_occurence[1])

    def highlight(self, range_start, range_end):
        self.props.buffer.select_range(range_start, range_end)
        # Focus
        self.scroll_mark_onscreen(self.mark)
__init__(self, buffer=None, autoscroll=True) special
Source code in lutris/gui/widgets/log_text_view.py
def __init__(self, buffer=None, autoscroll=True):
    super().__init__(visible=True)

    if buffer:
        self.set_buffer(buffer)
    self.set_editable(False)
    self.set_cursor_visible(False)
    self.set_monospace(True)
    self.set_left_margin(10)
    self.scroll_max = 0
    self.set_wrap_mode(Gtk.WrapMode.CHAR)
    self.get_style_context().add_class("lutris-logview")

    self.mark = self.create_new_mark(self.props.buffer.get_start_iter())

    if autoscroll:
        self.connect("size-allocate", self.autoscroll)
autoscroll(self, *args)
Source code in lutris/gui/widgets/log_text_view.py
def autoscroll(self, *args):  # pylint: disable=unused-argument
    adj = self.get_vadjustment()
    if adj.get_value() == self.scroll_max or self.scroll_max == 0:
        adj.set_value(adj.get_upper() - adj.get_page_size())
        self.scroll_max = adj.get_value()
    else:
        self.scroll_max = adj.get_upper() - adj.get_page_size()
create_new_mark(self, buffer_iter)
Source code in lutris/gui/widgets/log_text_view.py
def create_new_mark(self, buffer_iter):
    return self.props.buffer.create_mark(None, buffer_iter, True)
find_first(self, searched_entry)
Source code in lutris/gui/widgets/log_text_view.py
def find_first(self, searched_entry):
    self.reset_search()
    self.find_next(searched_entry)
find_next(self, searched_entry)
Source code in lutris/gui/widgets/log_text_view.py
def find_next(self, searched_entry):
    buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
    next_occurence = buffer_iter.forward_search(
        searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
    )

    # Found nothing try from the beginning
    if next_occurence is None:
        next_occurence = self.props.buffer.get_start_iter(
        ).forward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)

    # Highlight if result
    if next_occurence is not None:
        self.highlight(next_occurence[0], next_occurence[1])
        self.props.buffer.delete_mark(self.mark)
        self.mark = self.create_new_mark(next_occurence[1])
find_previous(self, searched_entry)
Source code in lutris/gui/widgets/log_text_view.py
def find_previous(self, searched_entry):
    # First go to the beginning of searched_entry string
    buffer_iter = self.props.buffer.get_iter_at_mark(self.mark)
    buffer_iter.backward_chars(len(searched_entry.get_text()))

    previous_occurence = buffer_iter.backward_search(
        searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None
    )

    # Found nothing ? Try from the end
    if previous_occurence is None:
        previous_occurence = self.props.buffer.get_end_iter(
        ).backward_search(searched_entry.get_text(), Gtk.TextSearchFlags.CASE_INSENSITIVE, None)

    # Highlight if result
    if previous_occurence is not None:
        self.highlight(previous_occurence[0], previous_occurence[1])
        self.props.buffer.delete_mark(self.mark)
        self.mark = self.create_new_mark(previous_occurence[1])
highlight(self, range_start, range_end)
Source code in lutris/gui/widgets/log_text_view.py
def highlight(self, range_start, range_end):
    self.props.buffer.select_range(range_start, range_end)
    # Focus
    self.scroll_mark_onscreen(self.mark)
Source code in lutris/gui/widgets/log_text_view.py
def reset_search(self):
    self.props.buffer.delete_mark(self.mark)
    self.mark = self.create_new_mark(self.props.buffer.get_start_iter())
    self.props.buffer.place_cursor(self.props.buffer.get_iter_at_mark(self.mark))

notifications

send_notification(title, text, file_path_to_icon='lutris')
Source code in lutris/gui/widgets/notifications.py
def send_notification(title, text, file_path_to_icon="lutris"):
    icon_file = Gio.File.new_for_path(file_path_to_icon)
    icon = Gio.FileIcon.new(icon_file)
    notification = Gio.Notification.new(title)
    notification.set_body(text)
    notification.set_icon(icon)

    application = Gio.Application.get_default()
    application.send_notification(None, notification)

    logger.info(title)
    logger.info(text)

searchable_combobox

Extended combobox with search

SearchableCombobox (Bin)

Combox box with autocompletion. Well fitted for large lists.

Source code in lutris/gui/widgets/searchable_combobox.py
class SearchableCombobox(Gtk.Bin):
    """Combox box with autocompletion.
    Well fitted for large lists.
    """

    __gsignals__ = {
        "changed": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
    }

    def __init__(self, choice_func, initial=None):
        super().__init__()
        self.initial = initial
        self.liststore = Gtk.ListStore(str, str)
        self.combobox = Gtk.ComboBox.new_with_model_and_entry(self.liststore)
        self.combobox.set_entry_text_column(0)
        self.combobox.set_id_column(1)
        self.combobox.set_valign(Gtk.Align.CENTER)

        completion = Gtk.EntryCompletion()
        completion.set_model(self.liststore)
        completion.set_text_column(0)
        completion.set_match_func(self.search_store)
        completion.connect("match-selected", self.set_id_from_completion)
        entry = self.combobox.get_child()
        entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, "content-loading-symbolic")
        entry.set_completion(completion)

        self.combobox.connect("changed", self.on_combobox_change)
        self.combobox.connect("scroll-event", self._on_combobox_scroll)
        self.add(self.combobox)
        GLib.idle_add(self._populate_combobox_choices, choice_func)

    def get_model(self):
        """Proxy to the liststore"""
        return self.liststore

    def get_active(self):
        """Proxy to the get_active method"""
        return self.combobox.get_active()

    @staticmethod
    def get_has_entry():
        """The entry present is not for editing custom values, only search"""
        return False

    def search_store(self, _completion, string, _iter):
        """Return true if any word of a string is present in a row"""
        for word in string.split():
            if word not in self.liststore[_iter][0].lower():  # search is always lower case
                return False
        return True

    def set_id_from_completion(self, _completion, model, _iter):
        """Sets the active ID to the appropriate ID column in the model
        otherwise the value is set to the entry's value.
        """
        self.combobox.set_active_id(model[_iter][1])

    def _populate_combobox_choices(self, choice_func):
        AsyncCall(self._do_populate_combobox_choices, None, choice_func)

    def _do_populate_combobox_choices(self, choice_func):
        for choice in choice_func():
            self.liststore.append(choice)
        entry = self.combobox.get_child()
        entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, None)
        self.combobox.set_active_id(self.initial)

    @staticmethod
    def _on_combobox_scroll(combobox, _event):
        """Prevents users from accidentally changing configuration values
        while scrolling down dialogs.
        """
        combobox.stop_emission_by_name("scroll-event")
        return False

    def on_combobox_change(self, _widget):
        """Action triggered on combobox 'changed' signal."""
        active = self.combobox.get_active()
        if active < 0:
            return
        option_value = self.liststore[active][1]
        self.emit("changed", option_value)
__init__(self, choice_func, initial=None) special
Source code in lutris/gui/widgets/searchable_combobox.py
def __init__(self, choice_func, initial=None):
    super().__init__()
    self.initial = initial
    self.liststore = Gtk.ListStore(str, str)
    self.combobox = Gtk.ComboBox.new_with_model_and_entry(self.liststore)
    self.combobox.set_entry_text_column(0)
    self.combobox.set_id_column(1)
    self.combobox.set_valign(Gtk.Align.CENTER)

    completion = Gtk.EntryCompletion()
    completion.set_model(self.liststore)
    completion.set_text_column(0)
    completion.set_match_func(self.search_store)
    completion.connect("match-selected", self.set_id_from_completion)
    entry = self.combobox.get_child()
    entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, "content-loading-symbolic")
    entry.set_completion(completion)

    self.combobox.connect("changed", self.on_combobox_change)
    self.combobox.connect("scroll-event", self._on_combobox_scroll)
    self.add(self.combobox)
    GLib.idle_add(self._populate_combobox_choices, choice_func)
get_active(self)

Proxy to the get_active method

Source code in lutris/gui/widgets/searchable_combobox.py
def get_active(self):
    """Proxy to the get_active method"""
    return self.combobox.get_active()
get_has_entry() staticmethod

The entry present is not for editing custom values, only search

Source code in lutris/gui/widgets/searchable_combobox.py
@staticmethod
def get_has_entry():
    """The entry present is not for editing custom values, only search"""
    return False
get_model(self)

Proxy to the liststore

Source code in lutris/gui/widgets/searchable_combobox.py
def get_model(self):
    """Proxy to the liststore"""
    return self.liststore
on_combobox_change(self, _widget)

Action triggered on combobox 'changed' signal.

Source code in lutris/gui/widgets/searchable_combobox.py
def on_combobox_change(self, _widget):
    """Action triggered on combobox 'changed' signal."""
    active = self.combobox.get_active()
    if active < 0:
        return
    option_value = self.liststore[active][1]
    self.emit("changed", option_value)
search_store(self, _completion, string, _iter)

Return true if any word of a string is present in a row

Source code in lutris/gui/widgets/searchable_combobox.py
def search_store(self, _completion, string, _iter):
    """Return true if any word of a string is present in a row"""
    for word in string.split():
        if word not in self.liststore[_iter][0].lower():  # search is always lower case
            return False
    return True
set_id_from_completion(self, _completion, model, _iter)

Sets the active ID to the appropriate ID column in the model otherwise the value is set to the entry's value.

Source code in lutris/gui/widgets/searchable_combobox.py
def set_id_from_completion(self, _completion, model, _iter):
    """Sets the active ID to the appropriate ID column in the model
    otherwise the value is set to the entry's value.
    """
    self.combobox.set_active_id(model[_iter][1])

sidebar

Sidebar for the main window

GAMECOUNT
ICON
LABEL
SLUG
TYPE
DummyRow

Dummy class for rows that may not be initialized.

Source code in lutris/gui/widgets/sidebar.py
class DummyRow():
    """Dummy class for rows that may not be initialized."""

    def show(self):
        """Dummy method for showing the row"""

    def hide(self):
        """Dummy method for hiding the row"""
hide(self)

Dummy method for hiding the row

Source code in lutris/gui/widgets/sidebar.py
def hide(self):
    """Dummy method for hiding the row"""
show(self)

Dummy method for showing the row

Source code in lutris/gui/widgets/sidebar.py
def show(self):
    """Dummy method for showing the row"""
LutrisSidebar (ListBox)
Source code in lutris/gui/widgets/sidebar.py
class LutrisSidebar(Gtk.ListBox):
    __gtype_name__ = "LutrisSidebar"

    def __init__(self, application, selected=None):
        super().__init__()
        self.set_size_request(200, -1)
        self.application = application
        self.get_style_context().add_class("sidebar")
        self.installed_runners = []
        self.service_rows = {}
        self.active_platforms = None
        self.runners = None
        self.platforms = None
        self.categories = None
        # A dummy objects that allows inspecting why/when we have a show() call on the object.
        self.running_row = DummyRow()
        if selected:
            self.selected_row_type, self.selected_row_id = selected.split(":")
        else:
            self.selected_row_type, self.selected_row_id = ("category", "all")
        self.row_headers = {
            "library": SidebarHeader(_("Library")),
            "sources": SidebarHeader(_("Sources")),
            "runners": SidebarHeader(_("Runners")),
            "platforms": SidebarHeader(_("Platforms")),
        }
        GObject.add_emission_hook(RunnerBox, "runner-installed", self.update)
        GObject.add_emission_hook(RunnerBox, "runner-removed", self.update)
        GObject.add_emission_hook(ServicesBox, "services-changed", self.on_services_changed)
        GObject.add_emission_hook(Game, "game-start", self.on_game_start)
        GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
        GObject.add_emission_hook(Game, "game-updated", self.update)
        GObject.add_emission_hook(Game, "game-removed", self.update)
        GObject.add_emission_hook(BaseService, "service-login", self.on_service_auth_changed)
        GObject.add_emission_hook(BaseService, "service-logout", self.on_service_auth_changed)
        GObject.add_emission_hook(BaseService, "service-games-load", self.on_service_games_updating)
        GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
        self.set_filter_func(self._filter_func)
        self.set_header_func(self._header_func)
        self.show_all()

    def get_sidebar_icon(self, icon_name):
        name = icon_name if has_stock_icon(icon_name) else "package-x-generic-symbolic"
        icon = Gtk.Image.new_from_icon_name(name, Gtk.IconSize.MENU)

        # We can wind up with an icon of the wrong size, if that's what is
        # available. So we'll fix that.
        icon_size = Gtk.IconSize.lookup(Gtk.IconSize.MENU)
        if icon_size[0]:
            icon.set_pixel_size(icon_size[2])

        return icon

    def initialize_rows(self):
        """
        Select the initial row; this triggers the initialization of the game view
        so we must do this even if this sidebar is never realized, but only after
        the sidebar's signals are connected.
        """
        self.active_platforms = games_db.get_used_platforms()
        self.runners = sorted(runners.__all__)
        self.platforms = sorted(runners.RUNNER_PLATFORMS)
        self.categories = categories_db.get_categories()

        self.add(
            SidebarRow(
                "all",
                "category",
                _("Games"),
                Gtk.Image.new_from_icon_name("applications-games-symbolic", Gtk.IconSize.MENU)
            )
        )

        self.add(
            SidebarRow(
                "recent",
                "dynamic_category",
                _("Recent"),
                Gtk.Image.new_from_icon_name("document-open-recent-symbolic", Gtk.IconSize.MENU)
            )
        )

        self.add(
            SidebarRow(
                "favorite",
                "category",
                _("Favorites"),
                Gtk.Image.new_from_icon_name("favorite-symbolic", Gtk.IconSize.MENU)
            )
        )

        self.running_row = SidebarRow(
            "running",
            "dynamic_category",
            _("Running"),
            Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.MENU)
        )
        # I wanted this to be on top but it really messes with the headers when showing/hiding the row.
        self.add(self.running_row)

        service_classes = services.get_enabled_services()
        for service_name in service_classes:
            service = service_classes[service_name]()
            row_class = OnlineServiceSidebarRow if service.online else ServiceSidebarRow
            service_row = row_class(service)
            self.service_rows[service_name] = service_row
            self.add(service_row)

        for runner_name in self.runners:
            icon_name = runner_name.lower().replace(" ", "") + "-symbolic"
            runner = runners.import_runner(runner_name)()
            self.add(RunnerSidebarRow(
                runner_name,
                "runner",
                runner.human_name,
                self.get_sidebar_icon(icon_name),
                application=self.application
            ))

        for platform in self.platforms:
            icon_name = (platform.lower().replace(" ", "").replace("/", "_") + "-symbolic")
            self.add(SidebarRow(platform, "platform", platform, self.get_sidebar_icon(icon_name)))

        self.update()

        for row in self.get_children():
            if row.type == self.selected_row_type and row.id == self.selected_row_id:
                self.select_row(row)
                break

        self.show_all()
        self.running_row.hide()

    def _filter_func(self, row):
        if not row or not row.id or row.type in ("category", "dynamic_category", "service"):
            return True
        if row.type == "runner":
            if row.id is None:
                return True  # 'All'
            return row.id in self.installed_runners
        return row.id in self.active_platforms

    def _header_func(self, row, before):
        if not before:
            row.set_header(self.row_headers["library"])
        elif before.type in ("category", "dynamic_category") and row.type == "service":
            row.set_header(self.row_headers["sources"])
        elif before.type == "service" and row.type == "runner":
            row.set_header(self.row_headers["runners"])
        elif before.type == "runner" and row.type == "platform":
            row.set_header(self.row_headers["platforms"])
        else:
            row.set_header(None)

    def update(self, *_args):
        self.installed_runners = [runner.name for runner in runners.get_installed()]
        self.active_platforms = games_db.get_used_platforms()
        self.invalidate_filter()
        return True

    def on_game_start(self, _game):
        """Show the "running" section when a game start"""
        self.running_row.show()
        return True

    def on_game_stop(self, _game):
        """Hide the "running" section when no games are running"""
        if not self.application.running_games.get_n_items():
            self.running_row.hide()

            if self.get_selected_row() == self.running_row:
                self.select_row(self.get_children()[0])

        return True

    def on_service_auth_changed(self, service):
        self.service_rows[service.id].create_button_box()
        self.service_rows[service.id].update_buttons()
        return True

    def on_service_games_updating(self, service):
        self.service_rows[service.id].is_updating = True
        self.service_rows[service.id].update_buttons()
        return True

    def on_service_games_updated(self, service):
        self.service_rows[service.id].is_updating = False
        self.service_rows[service.id].update_buttons()
        return True

    def on_services_changed(self, _widget):
        for child in self.get_children():
            child.destroy()
        self.initialize_rows()
        return True
__gtype_name__ special
__init__(self, application, selected=None) special
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, application, selected=None):
    super().__init__()
    self.set_size_request(200, -1)
    self.application = application
    self.get_style_context().add_class("sidebar")
    self.installed_runners = []
    self.service_rows = {}
    self.active_platforms = None
    self.runners = None
    self.platforms = None
    self.categories = None
    # A dummy objects that allows inspecting why/when we have a show() call on the object.
    self.running_row = DummyRow()
    if selected:
        self.selected_row_type, self.selected_row_id = selected.split(":")
    else:
        self.selected_row_type, self.selected_row_id = ("category", "all")
    self.row_headers = {
        "library": SidebarHeader(_("Library")),
        "sources": SidebarHeader(_("Sources")),
        "runners": SidebarHeader(_("Runners")),
        "platforms": SidebarHeader(_("Platforms")),
    }
    GObject.add_emission_hook(RunnerBox, "runner-installed", self.update)
    GObject.add_emission_hook(RunnerBox, "runner-removed", self.update)
    GObject.add_emission_hook(ServicesBox, "services-changed", self.on_services_changed)
    GObject.add_emission_hook(Game, "game-start", self.on_game_start)
    GObject.add_emission_hook(Game, "game-stop", self.on_game_stop)
    GObject.add_emission_hook(Game, "game-updated", self.update)
    GObject.add_emission_hook(Game, "game-removed", self.update)
    GObject.add_emission_hook(BaseService, "service-login", self.on_service_auth_changed)
    GObject.add_emission_hook(BaseService, "service-logout", self.on_service_auth_changed)
    GObject.add_emission_hook(BaseService, "service-games-load", self.on_service_games_updating)
    GObject.add_emission_hook(BaseService, "service-games-loaded", self.on_service_games_updated)
    self.set_filter_func(self._filter_func)
    self.set_header_func(self._header_func)
    self.show_all()
get_sidebar_icon(self, icon_name)
Source code in lutris/gui/widgets/sidebar.py
def get_sidebar_icon(self, icon_name):
    name = icon_name if has_stock_icon(icon_name) else "package-x-generic-symbolic"
    icon = Gtk.Image.new_from_icon_name(name, Gtk.IconSize.MENU)

    # We can wind up with an icon of the wrong size, if that's what is
    # available. So we'll fix that.
    icon_size = Gtk.IconSize.lookup(Gtk.IconSize.MENU)
    if icon_size[0]:
        icon.set_pixel_size(icon_size[2])

    return icon
initialize_rows(self)

Select the initial row; this triggers the initialization of the game view so we must do this even if this sidebar is never realized, but only after the sidebar's signals are connected.

Source code in lutris/gui/widgets/sidebar.py
def initialize_rows(self):
    """
    Select the initial row; this triggers the initialization of the game view
    so we must do this even if this sidebar is never realized, but only after
    the sidebar's signals are connected.
    """
    self.active_platforms = games_db.get_used_platforms()
    self.runners = sorted(runners.__all__)
    self.platforms = sorted(runners.RUNNER_PLATFORMS)
    self.categories = categories_db.get_categories()

    self.add(
        SidebarRow(
            "all",
            "category",
            _("Games"),
            Gtk.Image.new_from_icon_name("applications-games-symbolic", Gtk.IconSize.MENU)
        )
    )

    self.add(
        SidebarRow(
            "recent",
            "dynamic_category",
            _("Recent"),
            Gtk.Image.new_from_icon_name("document-open-recent-symbolic", Gtk.IconSize.MENU)
        )
    )

    self.add(
        SidebarRow(
            "favorite",
            "category",
            _("Favorites"),
            Gtk.Image.new_from_icon_name("favorite-symbolic", Gtk.IconSize.MENU)
        )
    )

    self.running_row = SidebarRow(
        "running",
        "dynamic_category",
        _("Running"),
        Gtk.Image.new_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.MENU)
    )
    # I wanted this to be on top but it really messes with the headers when showing/hiding the row.
    self.add(self.running_row)

    service_classes = services.get_enabled_services()
    for service_name in service_classes:
        service = service_classes[service_name]()
        row_class = OnlineServiceSidebarRow if service.online else ServiceSidebarRow
        service_row = row_class(service)
        self.service_rows[service_name] = service_row
        self.add(service_row)

    for runner_name in self.runners:
        icon_name = runner_name.lower().replace(" ", "") + "-symbolic"
        runner = runners.import_runner(runner_name)()
        self.add(RunnerSidebarRow(
            runner_name,
            "runner",
            runner.human_name,
            self.get_sidebar_icon(icon_name),
            application=self.application
        ))

    for platform in self.platforms:
        icon_name = (platform.lower().replace(" ", "").replace("/", "_") + "-symbolic")
        self.add(SidebarRow(platform, "platform", platform, self.get_sidebar_icon(icon_name)))

    self.update()

    for row in self.get_children():
        if row.type == self.selected_row_type and row.id == self.selected_row_id:
            self.select_row(row)
            break

    self.show_all()
    self.running_row.hide()
on_game_start(self, _game)

Show the "running" section when a game start

Source code in lutris/gui/widgets/sidebar.py
def on_game_start(self, _game):
    """Show the "running" section when a game start"""
    self.running_row.show()
    return True
on_game_stop(self, _game)

Hide the "running" section when no games are running

Source code in lutris/gui/widgets/sidebar.py
def on_game_stop(self, _game):
    """Hide the "running" section when no games are running"""
    if not self.application.running_games.get_n_items():
        self.running_row.hide()

        if self.get_selected_row() == self.running_row:
            self.select_row(self.get_children()[0])

    return True
on_service_auth_changed(self, service)
Source code in lutris/gui/widgets/sidebar.py
def on_service_auth_changed(self, service):
    self.service_rows[service.id].create_button_box()
    self.service_rows[service.id].update_buttons()
    return True
on_service_games_updated(self, service)
Source code in lutris/gui/widgets/sidebar.py
def on_service_games_updated(self, service):
    self.service_rows[service.id].is_updating = False
    self.service_rows[service.id].update_buttons()
    return True
on_service_games_updating(self, service)
Source code in lutris/gui/widgets/sidebar.py
def on_service_games_updating(self, service):
    self.service_rows[service.id].is_updating = True
    self.service_rows[service.id].update_buttons()
    return True
on_services_changed(self, _widget)
Source code in lutris/gui/widgets/sidebar.py
def on_services_changed(self, _widget):
    for child in self.get_children():
        child.destroy()
    self.initialize_rows()
    return True
update(self, *_args)
Source code in lutris/gui/widgets/sidebar.py
def update(self, *_args):
    self.installed_runners = [runner.name for runner in runners.get_installed()]
    self.active_platforms = games_db.get_used_platforms()
    self.invalidate_filter()
    return True
OnlineServiceSidebarRow (ServiceSidebarRow)
Source code in lutris/gui/widgets/sidebar.py
class OnlineServiceSidebarRow(ServiceSidebarRow):
    def get_buttons(self):
        return {
            "run": (("media-playback-start-symbolic", _("Run"), self.on_service_run, "run")),
            "refresh": ("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh"),
            "disconnect": ("system-log-out-symbolic", _("Disconnect"), self.on_connect_clicked, "disconnect"),
            "connect": ("avatar-default-symbolic", _("Connect"), self.on_connect_clicked, "connect")
        }

    def get_actions(self):
        buttons = self.get_buttons()
        displayed_buttons = []
        if self.service.is_launchable():
            displayed_buttons.append(buttons["run"])
        if self.service.is_authenticated():
            displayed_buttons += [buttons["refresh"], buttons["disconnect"]]
        else:
            displayed_buttons += [buttons["connect"]]
        return displayed_buttons

    def on_connect_clicked(self, button):
        button.set_sensitive(False)
        if self.service.is_authenticated():
            self.service.logout()
        else:
            self.service.login()
        self.create_button_box()
get_actions(self)

Return the definition of buttons to be added to the row

Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
    buttons = self.get_buttons()
    displayed_buttons = []
    if self.service.is_launchable():
        displayed_buttons.append(buttons["run"])
    if self.service.is_authenticated():
        displayed_buttons += [buttons["refresh"], buttons["disconnect"]]
    else:
        displayed_buttons += [buttons["connect"]]
    return displayed_buttons
get_buttons(self)
Source code in lutris/gui/widgets/sidebar.py
def get_buttons(self):
    return {
        "run": (("media-playback-start-symbolic", _("Run"), self.on_service_run, "run")),
        "refresh": ("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh"),
        "disconnect": ("system-log-out-symbolic", _("Disconnect"), self.on_connect_clicked, "disconnect"),
        "connect": ("avatar-default-symbolic", _("Connect"), self.on_connect_clicked, "connect")
    }
on_connect_clicked(self, button)
Source code in lutris/gui/widgets/sidebar.py
def on_connect_clicked(self, button):
    button.set_sensitive(False)
    if self.service.is_authenticated():
        self.service.logout()
    else:
        self.service.login()
    self.create_button_box()
RunnerSidebarRow (SidebarRow)
Source code in lutris/gui/widgets/sidebar.py
class RunnerSidebarRow(SidebarRow):
    def get_actions(self):
        """Return the definition of buttons to be added to the row"""
        if not self.id:
            return []
        entries = []

        # Creation is delayed because only installed runners can be imported
        # and all visible boxes should be installed.
        self.runner = runners.import_runner(self.id)()
        if self.runner.multiple_versions:
            entries.append((
                "system-software-install-symbolic",
                _("Manage Versions"),
                self.on_manage_versions,
                "manage-versions"
            ))
        if self.runner.runnable_alone:
            entries.append(("media-playback-start-symbolic", _("Run"), self.runner.run, "run"))
        entries.append(("emblem-system-symbolic", _("Configure"), self.on_configure_runner, "configure"))
        return entries

    def on_configure_runner(self, *_args):
        """Show runner configuration"""
        self.application.show_window(RunnerConfigDialog, runner=self.runner)

    def on_manage_versions(self, *_args):
        """Manage runner versions"""
        dlg_title = _("Manage %s versions") % self.runner.name
        RunnerInstallDialog(dlg_title, self.get_toplevel(), self.runner.name)
get_actions(self)

Return the definition of buttons to be added to the row

Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
    """Return the definition of buttons to be added to the row"""
    if not self.id:
        return []
    entries = []

    # Creation is delayed because only installed runners can be imported
    # and all visible boxes should be installed.
    self.runner = runners.import_runner(self.id)()
    if self.runner.multiple_versions:
        entries.append((
            "system-software-install-symbolic",
            _("Manage Versions"),
            self.on_manage_versions,
            "manage-versions"
        ))
    if self.runner.runnable_alone:
        entries.append(("media-playback-start-symbolic", _("Run"), self.runner.run, "run"))
    entries.append(("emblem-system-symbolic", _("Configure"), self.on_configure_runner, "configure"))
    return entries
on_configure_runner(self, *_args)

Show runner configuration

Source code in lutris/gui/widgets/sidebar.py
def on_configure_runner(self, *_args):
    """Show runner configuration"""
    self.application.show_window(RunnerConfigDialog, runner=self.runner)
on_manage_versions(self, *_args)

Manage runner versions

Source code in lutris/gui/widgets/sidebar.py
def on_manage_versions(self, *_args):
    """Manage runner versions"""
    dlg_title = _("Manage %s versions") % self.runner.name
    RunnerInstallDialog(dlg_title, self.get_toplevel(), self.runner.name)
ServiceSidebarRow (SidebarRow)
Source code in lutris/gui/widgets/sidebar.py
class ServiceSidebarRow(SidebarRow):

    def __init__(self, service):
        super().__init__(
            service.id,
            "service",
            service.name,
            Gtk.Image.new_from_icon_name(service.icon, Gtk.IconSize.MENU)
        )
        self.service = service

    def get_actions(self):
        """Return the definition of buttons to be added to the row"""
        return [
            ("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh")
        ]

    def on_service_run(self, button):
        """Run a launcher associated with a service"""
        self.service.run()

    def on_refresh_clicked(self, button):
        """Reload the service games"""
        button.set_sensitive(False)
        if self.service.online and not self.service.is_connected():
            self.service.logout()
            return
        AsyncCall(self.service.reload, self.service_load_cb)

    def service_load_cb(self, _result, error):
        if error:
            if isinstance(error, AuthTokenExpired):
                self.service.logout()
                self.service.login()
            else:
                ErrorDialog(str(error))
        GLib.timeout_add(2000, self.enable_refresh_button)

    def enable_refresh_button(self):
        self.buttons["refresh"].set_sensitive(True)
        return False
__init__(self, service) special
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, service):
    super().__init__(
        service.id,
        "service",
        service.name,
        Gtk.Image.new_from_icon_name(service.icon, Gtk.IconSize.MENU)
    )
    self.service = service
enable_refresh_button(self)
Source code in lutris/gui/widgets/sidebar.py
def enable_refresh_button(self):
    self.buttons["refresh"].set_sensitive(True)
    return False
get_actions(self)

Return the definition of buttons to be added to the row

Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
    """Return the definition of buttons to be added to the row"""
    return [
        ("view-refresh-symbolic", _("Reload"), self.on_refresh_clicked, "refresh")
    ]
on_refresh_clicked(self, button)

Reload the service games

Source code in lutris/gui/widgets/sidebar.py
def on_refresh_clicked(self, button):
    """Reload the service games"""
    button.set_sensitive(False)
    if self.service.online and not self.service.is_connected():
        self.service.logout()
        return
    AsyncCall(self.service.reload, self.service_load_cb)
on_service_run(self, button)

Run a launcher associated with a service

Source code in lutris/gui/widgets/sidebar.py
def on_service_run(self, button):
    """Run a launcher associated with a service"""
    self.service.run()
service_load_cb(self, _result, error)
Source code in lutris/gui/widgets/sidebar.py
def service_load_cb(self, _result, error):
    if error:
        if isinstance(error, AuthTokenExpired):
            self.service.logout()
            self.service.login()
        else:
            ErrorDialog(str(error))
    GLib.timeout_add(2000, self.enable_refresh_button)
SidebarHeader (Box)

Header shown on top of each sidebar section

Source code in lutris/gui/widgets/sidebar.py
class SidebarHeader(Gtk.Box):
    """Header shown on top of each sidebar section"""

    def __init__(self, name):
        super().__init__(orientation=Gtk.Orientation.VERTICAL)
        self.get_style_context().add_class("sidebar-header")
        label = Gtk.Label(
            halign=Gtk.Align.START,
            hexpand=True,
            use_markup=True,
            label="<b>{}</b>".format(name),
        )
        label.get_style_context().add_class("dim-label")
        box = Gtk.Box(margin_start=9, margin_top=6, margin_bottom=6, margin_right=9)
        box.add(label)
        self.add(box)
        self.add(Gtk.Separator())
        self.show_all()
__init__(self, name) special
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, name):
    super().__init__(orientation=Gtk.Orientation.VERTICAL)
    self.get_style_context().add_class("sidebar-header")
    label = Gtk.Label(
        halign=Gtk.Align.START,
        hexpand=True,
        use_markup=True,
        label="<b>{}</b>".format(name),
    )
    label.get_style_context().add_class("dim-label")
    box = Gtk.Box(margin_start=9, margin_top=6, margin_bottom=6, margin_right=9)
    box.add(label)
    self.add(box)
    self.add(Gtk.Separator())
    self.show_all()
SidebarRow (ListBoxRow)

A row in the sidebar containing possible action buttons

Source code in lutris/gui/widgets/sidebar.py
class SidebarRow(Gtk.ListBoxRow):
    """A row in the sidebar containing possible action buttons"""
    MARGIN = 9
    SPACING = 6

    def __init__(self, id_, type_, name, icon, application=None):
        """Initialize the row

        Parameters:
            id_: identifier of the row
            type: type of row to display (still used?)
            name (str): Text displayed on the row
            icon (GtkImage): icon displayed next to the label
            application (GtkApplication): reference to the running application
        """
        super().__init__()
        self.application = application
        self.type = type_
        self.id = id_
        self.runner = None
        self.name = name
        self.is_updating = False
        self.buttons = {}
        self.box = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
        self.connect("realize", self.on_realize)
        self.add(self.box)

        if not icon:
            icon = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
        self.box.add(icon)
        label = Gtk.Label(
            label=name,
            halign=Gtk.Align.START,
            hexpand=True,
            margin_top=self.SPACING,
            margin_bottom=self.SPACING,
            ellipsize=Pango.EllipsizeMode.END,
        )
        self.box.pack_start(label, True, True, 0)
        self.btn_box = Gtk.Box(spacing=3, no_show_all=True, valign=Gtk.Align.CENTER, homogeneous=True)
        self.box.pack_end(self.btn_box, False, False, 0)
        self.spinner = Gtk.Spinner()
        self.box.pack_end(self.spinner, False, False, 0)

    def get_actions(self):
        return []

    def is_row_active(self):
        """Return true if the row is hovered or is the one selected"""
        flags = self.get_state_flags()
        # Naming things sure is hard... But "prelight" instead of "hover"? Come on...
        return flags & Gtk.StateFlags.PRELIGHT or flags & Gtk.StateFlags.SELECTED

    def do_state_flags_changed(self, previous_flags):  # pylint: disable=arguments-differ
        if self.id:
            self.update_buttons()
        Gtk.ListBoxRow.do_state_flags_changed(self, previous_flags)

    def update_buttons(self):
        if self.is_updating:
            self.btn_box.hide()
            self.spinner.show()
            self.spinner.start()
            return
        self.spinner.stop()
        self.spinner.hide()
        if self.is_row_active():
            self.btn_box.show()
        elif self.btn_box.get_visible():
            self.btn_box.hide()

    def create_button_box(self):
        """Adds buttons in the button box based on the row's actions"""
        for child in self.btn_box.get_children():
            child.destroy()
        for action in self.get_actions():
            btn = Gtk.Button(tooltip_text=action[1], relief=Gtk.ReliefStyle.NONE, visible=True)
            image = Gtk.Image.new_from_icon_name(action[0], Gtk.IconSize.MENU)
            image.show()
            btn.add(image)
            btn.connect("clicked", action[2])
            self.buttons[action[3]] = btn
            self.btn_box.add(btn)

    def on_realize(self, widget):
        self.create_button_box()
MARGIN
SPACING
__init__(self, id_, type_, name, icon, application=None) special

Initialize the row

Parameters:

Name Type Description Default
id_

identifier of the row

required
type

type of row to display (still used?)

required
name str

Text displayed on the row

required
icon GtkImage

icon displayed next to the label

required
application GtkApplication

reference to the running application

None
Source code in lutris/gui/widgets/sidebar.py
def __init__(self, id_, type_, name, icon, application=None):
    """Initialize the row

    Parameters:
        id_: identifier of the row
        type: type of row to display (still used?)
        name (str): Text displayed on the row
        icon (GtkImage): icon displayed next to the label
        application (GtkApplication): reference to the running application
    """
    super().__init__()
    self.application = application
    self.type = type_
    self.id = id_
    self.runner = None
    self.name = name
    self.is_updating = False
    self.buttons = {}
    self.box = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
    self.connect("realize", self.on_realize)
    self.add(self.box)

    if not icon:
        icon = Gtk.Box(spacing=self.SPACING, margin_start=self.MARGIN, margin_end=self.MARGIN)
    self.box.add(icon)
    label = Gtk.Label(
        label=name,
        halign=Gtk.Align.START,
        hexpand=True,
        margin_top=self.SPACING,
        margin_bottom=self.SPACING,
        ellipsize=Pango.EllipsizeMode.END,
    )
    self.box.pack_start(label, True, True, 0)
    self.btn_box = Gtk.Box(spacing=3, no_show_all=True, valign=Gtk.Align.CENTER, homogeneous=True)
    self.box.pack_end(self.btn_box, False, False, 0)
    self.spinner = Gtk.Spinner()
    self.box.pack_end(self.spinner, False, False, 0)
create_button_box(self)

Adds buttons in the button box based on the row's actions

Source code in lutris/gui/widgets/sidebar.py
def create_button_box(self):
    """Adds buttons in the button box based on the row's actions"""
    for child in self.btn_box.get_children():
        child.destroy()
    for action in self.get_actions():
        btn = Gtk.Button(tooltip_text=action[1], relief=Gtk.ReliefStyle.NONE, visible=True)
        image = Gtk.Image.new_from_icon_name(action[0], Gtk.IconSize.MENU)
        image.show()
        btn.add(image)
        btn.connect("clicked", action[2])
        self.buttons[action[3]] = btn
        self.btn_box.add(btn)
do_state_flags_changed(self, previous_flags)

state_flags_changed(self, previous_state_flags:Gtk.StateFlags)

Source code in lutris/gui/widgets/sidebar.py
def do_state_flags_changed(self, previous_flags):  # pylint: disable=arguments-differ
    if self.id:
        self.update_buttons()
    Gtk.ListBoxRow.do_state_flags_changed(self, previous_flags)
get_actions(self)
Source code in lutris/gui/widgets/sidebar.py
def get_actions(self):
    return []
is_row_active(self)

Return true if the row is hovered or is the one selected

Source code in lutris/gui/widgets/sidebar.py
def is_row_active(self):
    """Return true if the row is hovered or is the one selected"""
    flags = self.get_state_flags()
    # Naming things sure is hard... But "prelight" instead of "hover"? Come on...
    return flags & Gtk.StateFlags.PRELIGHT or flags & Gtk.StateFlags.SELECTED
on_realize(self, widget)
Source code in lutris/gui/widgets/sidebar.py
def on_realize(self, widget):
    self.create_button_box()
update_buttons(self)
Source code in lutris/gui/widgets/sidebar.py
def update_buttons(self):
    if self.is_updating:
        self.btn_box.hide()
        self.spinner.show()
        self.spinner.start()
        return
    self.spinner.stop()
    self.spinner.hide()
    if self.is_row_active():
        self.btn_box.show()
    elif self.btn_box.get_visible():
        self.btn_box.hide()

status_icon

AppIndicator based tray icon

APP_INDICATOR_SUPPORTED
LutrisStatusIcon
Source code in lutris/gui/widgets/status_icon.py
class LutrisStatusIcon:

    def __init__(self, application):
        self.application = application
        self.icon = self.create()
        self.menu = self.get_menu()
        self.set_visible(True)
        if APP_INDICATOR_SUPPORTED:
            self.icon.set_menu(self.menu)
        else:
            self.icon.connect("activate", self.on_activate)
            self.icon.connect("popup-menu", self.on_menu_popup)

    def create(self):
        """Create an appindicator"""
        if APP_INDICATOR_SUPPORTED:
            return AppIndicator.Indicator.new(
                "net.lutris.Lutris", "lutris", AppIndicator.IndicatorCategory.APPLICATION_STATUS
            )
        return LutrisTray(self.application)

    def is_visible(self):
        """Whether the icon is visible"""
        if APP_INDICATOR_SUPPORTED:
            return self.icon.get_status() != AppIndicator.IndicatorStatus.PASSIVE
        return self.icon.is_visible()

    def set_visible(self, value):
        """Set the visibility of the icon"""
        if APP_INDICATOR_SUPPORTED:
            if value:
                visible = AppIndicator.IndicatorStatus.ACTIVE
            else:
                visible = AppIndicator.IndicatorStatus.ACTIVE
            self.icon.set_status(visible)
        else:
            self.icon.set_visible(value)

    def get_menu(self):
        """Instanciates the menu attached to the tray icon"""
        menu = Gtk.Menu()
        installed_games = self.add_games()
        number_of_games_in_menu = 10
        for game in installed_games[:number_of_games_in_menu]:
            menu.append(self._make_menu_item_for_game(game))
        menu.append(Gtk.SeparatorMenuItem())

        present_menu = Gtk.ImageMenuItem()
        present_menu.set_image(Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU))
        present_menu.set_label(_("Show Lutris"))
        present_menu.connect("activate", self.on_activate)
        menu.append(present_menu)

        quit_menu = Gtk.MenuItem()
        quit_menu.set_label(_("Quit"))
        quit_menu.connect("activate", self.on_quit_application)
        menu.append(quit_menu)
        menu.show_all()
        return menu

    def on_activate(self, _status_icon, _event=None):
        """Callback to show or hide the window"""
        self.application.window.present()

    def on_menu_popup(self, _status_icon, button, time):
        """Callback to show the contextual menu"""
        self.menu.popup(None, None, None, None, button, time)

    def on_quit_application(self, _widget):
        """Callback to quit the program"""
        self.application.do_shutdown()

    def _make_menu_item_for_game(self, game):
        menu_item = Gtk.MenuItem()
        menu_item.set_label(game["name"])
        menu_item.connect("activate", self.on_game_selected, game["id"])
        return menu_item

    @staticmethod
    def add_games():
        """Adds installed games in order of last use"""
        installed_games = get_games(filters={"installed": 1})
        installed_games.sort(
            key=lambda game: max(game["lastplayed"] or 0, game["installed_at"] or 0),
            reverse=True,
        )
        return installed_games

    def on_game_selected(self, _widget, game_id):
        Game(game_id).launch()
__init__(self, application) special
Source code in lutris/gui/widgets/status_icon.py
def __init__(self, application):
    self.application = application
    self.icon = self.create()
    self.menu = self.get_menu()
    self.set_visible(True)
    if APP_INDICATOR_SUPPORTED:
        self.icon.set_menu(self.menu)
    else:
        self.icon.connect("activate", self.on_activate)
        self.icon.connect("popup-menu", self.on_menu_popup)
add_games() staticmethod

Adds installed games in order of last use

Source code in lutris/gui/widgets/status_icon.py
@staticmethod
def add_games():
    """Adds installed games in order of last use"""
    installed_games = get_games(filters={"installed": 1})
    installed_games.sort(
        key=lambda game: max(game["lastplayed"] or 0, game["installed_at"] or 0),
        reverse=True,
    )
    return installed_games
create(self)

Create an appindicator

Source code in lutris/gui/widgets/status_icon.py
def create(self):
    """Create an appindicator"""
    if APP_INDICATOR_SUPPORTED:
        return AppIndicator.Indicator.new(
            "net.lutris.Lutris", "lutris", AppIndicator.IndicatorCategory.APPLICATION_STATUS
        )
    return LutrisTray(self.application)
get_menu(self)

Instanciates the menu attached to the tray icon

Source code in lutris/gui/widgets/status_icon.py
def get_menu(self):
    """Instanciates the menu attached to the tray icon"""
    menu = Gtk.Menu()
    installed_games = self.add_games()
    number_of_games_in_menu = 10
    for game in installed_games[:number_of_games_in_menu]:
        menu.append(self._make_menu_item_for_game(game))
    menu.append(Gtk.SeparatorMenuItem())

    present_menu = Gtk.ImageMenuItem()
    present_menu.set_image(Gtk.Image.new_from_icon_name("lutris", Gtk.IconSize.MENU))
    present_menu.set_label(_("Show Lutris"))
    present_menu.connect("activate", self.on_activate)
    menu.append(present_menu)

    quit_menu = Gtk.MenuItem()
    quit_menu.set_label(_("Quit"))
    quit_menu.connect("activate", self.on_quit_application)
    menu.append(quit_menu)
    menu.show_all()
    return menu
is_visible(self)

Whether the icon is visible

Source code in lutris/gui/widgets/status_icon.py
def is_visible(self):
    """Whether the icon is visible"""
    if APP_INDICATOR_SUPPORTED:
        return self.icon.get_status() != AppIndicator.IndicatorStatus.PASSIVE
    return self.icon.is_visible()
on_activate(self, _status_icon, _event=None)

Callback to show or hide the window

Source code in lutris/gui/widgets/status_icon.py
def on_activate(self, _status_icon, _event=None):
    """Callback to show or hide the window"""
    self.application.window.present()
on_game_selected(self, _widget, game_id)
Source code in lutris/gui/widgets/status_icon.py
def on_game_selected(self, _widget, game_id):
    Game(game_id).launch()
on_menu_popup(self, _status_icon, button, time)

Callback to show the contextual menu

Source code in lutris/gui/widgets/status_icon.py
def on_menu_popup(self, _status_icon, button, time):
    """Callback to show the contextual menu"""
    self.menu.popup(None, None, None, None, button, time)
on_quit_application(self, _widget)

Callback to quit the program

Source code in lutris/gui/widgets/status_icon.py
def on_quit_application(self, _widget):
    """Callback to quit the program"""
    self.application.do_shutdown()
set_visible(self, value)

Set the visibility of the icon

Source code in lutris/gui/widgets/status_icon.py
def set_visible(self, value):
    """Set the visibility of the icon"""
    if APP_INDICATOR_SUPPORTED:
        if value:
            visible = AppIndicator.IndicatorStatus.ACTIVE
        else:
            visible = AppIndicator.IndicatorStatus.ACTIVE
        self.icon.set_status(visible)
    else:
        self.icon.set_visible(value)
LutrisTray (StatusIcon)

Lutris tray icon

Source code in lutris/gui/widgets/status_icon.py
class LutrisTray(Gtk.StatusIcon):

    """Lutris tray icon"""

    def __init__(self, application, **_kwargs):
        super().__init__()
        self.set_tooltip_text(_("Lutris"))
        self.set_visible(True)
        self.application = application
        self.set_from_icon_name("lutris")
__init__(self, application, **_kwargs) special
Source code in lutris/gui/widgets/status_icon.py
def __init__(self, application, **_kwargs):
    super().__init__()
    self.set_tooltip_text(_("Lutris"))
    self.set_visible(True)
    self.application = application
    self.set_from_icon_name("lutris")

utils

Various utilities using the GObject framework

BANNER_SIZE
ICON_SIZE
convert_to_background(background_path, target_size=(320, 1080))

Converts a image to a pane background

Source code in lutris/gui/widgets/utils.py
def convert_to_background(background_path, target_size=(320, 1080)):
    """Converts a image to a pane background"""
    coverart = Image.open(background_path)
    coverart = coverart.convert("RGBA")

    target_width, target_height = target_size
    image_height = int(target_height * 0.80)  # 80% of the mask is visible
    orig_width, orig_height = coverart.size

    # Resize and crop coverart
    width = int(orig_width * (image_height / orig_height))
    offset = int((width - target_width) / 2)
    coverart = coverart.resize((width, image_height), resample=Image.BICUBIC)
    coverart = coverart.crop((offset, 0, target_width + offset, image_height))

    # Resize canvas of coverart by putting transparent pixels on the bottom
    coverart_bg = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 0))
    coverart_bg.paste(coverart, (0, 0, target_width, image_height))

    # Apply a tint to the base image
    # tint = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 255))
    # coverart = Image.blend(coverart, tint, 0.6)

    # Paste coverart on transparent image while applying a gradient mask
    background = Image.new('RGBA', (target_width, target_height), (0, 0, 0, 0))
    mask = Image.open(os.path.join(datapath.get(), "media/mask.png"))
    background.paste(coverart_bg, mask=mask)

    return background
get_default_icon(size)
Source code in lutris/gui/widgets/utils.py
def get_default_icon(size):
    if size[0] == size[1]:
        return os.path.join(datapath.get(), "media/default_icon.png")
    return os.path.join(datapath.get(), "media/default_banner.png")
get_icon(icon_name, icon_format='image', size=None, icon_type='runner')

Return an icon based on the given name, format, size and type.

icon_name -- The name of the icon to retrieve format -- The format of the icon, which should be either 'image' or 'pixbuf' (default 'image') size -- The size for the desired image (default None) icon_type -- Retrieve either a 'runner' or 'platform' icon (default 'runner')

Source code in lutris/gui/widgets/utils.py
def get_icon(icon_name, icon_format="image", size=None, icon_type="runner"):
    """Return an icon based on the given name, format, size and type.

    Keyword arguments:
    icon_name -- The name of the icon to retrieve
    format -- The format of the icon, which should be either 'image' or 'pixbuf' (default 'image')
    size -- The size for the desired image (default None)
    icon_type -- Retrieve either a 'runner' or 'platform' icon (default 'runner')
    """
    filename = icon_name.lower().replace(" ", "") + ".png"
    icon_path = os.path.join(settings.RUNTIME_DIR, "icons/hicolor/64x64/apps", filename)
    if not os.path.exists(icon_path):
        return None
    if icon_format == "image":
        icon = Gtk.Image()
        if size:
            icon.set_from_pixbuf(get_pixbuf(icon_path, size))
        else:
            icon.set_from_file(icon_path)
        return icon
    if icon_format == "pixbuf" and size:
        return get_pixbuf(icon_path, size)
    raise ValueError("Invalid arguments")

Return a transparent text button for the side panels

Source code in lutris/gui/widgets/utils.py
def get_link_button(text):
    """Return a transparent text button for the side panels"""
    button = Gtk.Button(text, visible=True)
    button.props.relief = Gtk.ReliefStyle.NONE
    button.get_children()[0].set_alignment(0, 0.5)
    button.get_style_context().add_class("panel-button")
    button.set_size_request(-1, 24)
    return button
get_main_window(widget)

Return the application's main window from one of its widget

Source code in lutris/gui/widgets/utils.py
def get_main_window(widget):
    """Return the application's main window from one of its widget"""
    parent = widget.get_toplevel()
    if not isinstance(parent, Gtk.Window):
        # The sync dialog may have closed
        parent = Gio.Application.get_default().props.active_window
    for window in parent.application.get_windows():
        if "LutrisWindow" in window.__class__.__name__:
            return window
    return
get_overlay(overlay_path, size)
Source code in lutris/gui/widgets/utils.py
def get_overlay(overlay_path, size):
    width, height = size
    transparent_pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(overlay_path, width, height)
    transparent_pixbuf = transparent_pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.NEAREST)
    return transparent_pixbuf
get_pixbuf(image, size, fallback=None, is_installed=True)

Return a pixbuf from file image at size or fallback to fallback

Source code in lutris/gui/widgets/utils.py
def get_pixbuf(image, size, fallback=None, is_installed=True):
    """Return a pixbuf from file `image` at `size` or fallback to `fallback`"""
    width, height = size
    pixbuf = None
    if system.path_exists(image, exclude_empty=True):
        try:
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(image, width, height)
            pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.NEAREST)
        except GLib.GError:
            logger.error("Unable to load icon from image %s", image)
    else:
        if not fallback:
            fallback = get_default_icon(size)
        if system.path_exists(fallback):
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(fallback, width, height)
    if is_installed and pixbuf:
        pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.NEAREST)
        return pixbuf
    overlay = os.path.join(datapath.get(), "media/unavailable.png")
    transparent_pixbuf = get_overlay(overlay, size).copy()
    if pixbuf:
        pixbuf.composite(
            transparent_pixbuf,
            0,
            0,
            size[0],
            size[1],
            0,
            0,
            1,
            1,
            GdkPixbuf.InterpType.NEAREST,
            100,
        )
    return transparent_pixbuf
get_stock_icon(name, size)

Return a pixbuf from a stock icon name

Source code in lutris/gui/widgets/utils.py
def get_stock_icon(name, size):
    """Return a pixbuf from a stock icon name"""
    theme = Gtk.IconTheme.get_default()
    try:
        return theme.load_icon(name, size, Gtk.IconLookupFlags.GENERIC_FALLBACK)
    except GLib.GError:
        logger.error("Failed to read icon %s", name)
        return None
has_stock_icon(name)

This tests if a GTK stock icon is known; if not we can try a fallback.

Source code in lutris/gui/widgets/utils.py
def has_stock_icon(name):
    """This tests if a GTK stock icon is known; if not we can try a fallback."""
    theme = Gtk.IconTheme.get_default()
    return theme.has_icon(name)
image2pixbuf(image)

Converts a PIL Image to a GDK Pixbuf

Source code in lutris/gui/widgets/utils.py
def image2pixbuf(image):
    """Converts a PIL Image to a GDK Pixbuf"""
    image_array = array.array('B', image.tobytes())
    width, height = image.size
    return GdkPixbuf.Pixbuf.new_from_data(image_array, GdkPixbuf.Colorspace.RGB, True, 8, width, height, width * 4)
load_icon_theme()

Add the lutris icon folder to the default theme

Source code in lutris/gui/widgets/utils.py
def load_icon_theme():
    """Add the lutris icon folder to the default theme"""
    icon_theme = Gtk.IconTheme.get_default()
    local_theme_path = os.path.join(settings.RUNTIME_DIR, "icons")
    if local_theme_path not in icon_theme.get_search_path():
        icon_theme.prepend_search_path(local_theme_path)
open_uri(uri)

Opens a local or remote URI with the default application

Source code in lutris/gui/widgets/utils.py
def open_uri(uri):
    """Opens a local or remote URI with the default application"""
    system.reset_library_preloads()
    try:
        Gtk.show_uri(None, uri, Gdk.CURRENT_TIME)
    except GLib.Error as ex:
        logger.exception("Failed to open URI %s: %s, falling back to xdg-open", uri, ex)
        system.execute(["xdg-open", uri])
paste_overlay(base_image, overlay_image, position=0.7)
Source code in lutris/gui/widgets/utils.py
def paste_overlay(base_image, overlay_image, position=0.7):
    base_width, base_height = base_image.size
    overlay_width, overlay_height = overlay_image.size
    offset_x = int((base_width - overlay_width) / 2)
    offset_y = int((base_height - overlay_height) / 2)
    base_image.paste(
        overlay_image, (
            offset_x,
            offset_y,
            overlay_width + offset_x,
            overlay_height + offset_y
        ),
        mask=overlay_image
    )
    return base_image
thumbnail_image(base_image, target_size)
Source code in lutris/gui/widgets/utils.py
def thumbnail_image(base_image, target_size):
    base_width, base_height = base_image.size
    base_ratio = base_width / base_height
    target_width, target_height = target_size
    target_ratio = target_width / target_height

    # Resize and crop coverart
    if base_ratio >= target_ratio:
        width = int(base_width * (target_height / base_height))
        height = target_height
    else:
        width = target_width
        height = int(base_height * (target_width / base_width))
    x_offset = int((width - target_width) / 2)
    y_offset = int((height - target_height) / 2)
    base_image = base_image.resize((width, height), resample=Image.BICUBIC)
    base_image = base_image.crop((x_offset, y_offset, width - x_offset, height - y_offset))
    return base_image

window

BaseApplicationWindow (ApplicationWindow)

Window used to guide the user through a issue reporting process

Source code in lutris/gui/widgets/window.py
class BaseApplicationWindow(Gtk.ApplicationWindow):

    """Window used to guide the user through a issue reporting process"""

    def __init__(self, application):
        Gtk.ApplicationWindow.__init__(self, icon_name="lutris", application=application)
        self.application = application
        self.set_show_menubar(False)

        self.set_position(Gtk.WindowPosition.CENTER)
        self.connect("delete-event", self.on_destroy)

        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, visible=True)
        self.vbox.set_margin_top(18)
        self.vbox.set_margin_bottom(18)
        self.vbox.set_margin_right(18)
        self.vbox.set_margin_left(18)
        self.add(self.vbox)
        self.action_buttons = Gtk.Box(spacing=6)
        self.vbox.pack_end(self.action_buttons, False, False, 0)

    def get_action_button(self, label, handler=None, tooltip=None):
        """Returns a button that can be used for the action bar"""
        button = Gtk.Button.new_with_mnemonic(label)
        if handler:
            button.connect("clicked", handler)
        if tooltip:
            button.set_tooltip_text(tooltip)
        return button

    def on_destroy(self, _widget=None, _data=None):
        """Destroy callback"""
        self.destroy()

    def present(self):  # pylint: disable=arguments-differ
        """The base implementation doesn't always work, this one does."""
        self.set_keep_above(True)
        super().present()
        self.set_keep_above(False)
        super().present()
__init__(self, application) special
Source code in lutris/gui/widgets/window.py
def __init__(self, application):
    Gtk.ApplicationWindow.__init__(self, icon_name="lutris", application=application)
    self.application = application
    self.set_show_menubar(False)

    self.set_position(Gtk.WindowPosition.CENTER)
    self.connect("delete-event", self.on_destroy)

    self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, visible=True)
    self.vbox.set_margin_top(18)
    self.vbox.set_margin_bottom(18)
    self.vbox.set_margin_right(18)
    self.vbox.set_margin_left(18)
    self.add(self.vbox)
    self.action_buttons = Gtk.Box(spacing=6)
    self.vbox.pack_end(self.action_buttons, False, False, 0)
get_action_button(self, label, handler=None, tooltip=None)

Returns a button that can be used for the action bar

Source code in lutris/gui/widgets/window.py
def get_action_button(self, label, handler=None, tooltip=None):
    """Returns a button that can be used for the action bar"""
    button = Gtk.Button.new_with_mnemonic(label)
    if handler:
        button.connect("clicked", handler)
    if tooltip:
        button.set_tooltip_text(tooltip)
    return button
on_destroy(self, _widget=None, _data=None)

Destroy callback

Source code in lutris/gui/widgets/window.py
def on_destroy(self, _widget=None, _data=None):
    """Destroy callback"""
    self.destroy()
present(self)

The base implementation doesn't always work, this one does.

Source code in lutris/gui/widgets/window.py
def present(self):  # pylint: disable=arguments-differ
    """The base implementation doesn't always work, this one does."""
    self.set_keep_above(True)
    super().present()
    self.set_keep_above(False)
    super().present()

installer special

Install script interpreter package.

AUTO_ELF_EXE

AUTO_WIN32_EXE

get_installers(game_slug=None, installer_file=None, revision=None)

Source code in lutris/installer/__init__.py
def get_installers(game_slug=None, installer_file=None, revision=None):
    # check if installer is local or online
    if system.path_exists(installer_file):
        return read_script(installer_file)
    return get_game_installers(game_slug=game_slug, revision=revision)

read_script(filename)

Return scripts from a local file

Source code in lutris/installer/__init__.py
def read_script(filename):
    """Return scripts from a local file"""
    logger.debug("Loading script(s) from %s", filename)
    with open(filename, "r", encoding='utf-8') as local_file:
        script = yaml.safe_load(local_file.read())
        if isinstance(script, list):
            return script
        if "results" in script:
            return script["results"]
        return [script]

commands

Commands for installer scripts

CommandsMixin

The directives for the installer: part of the install script.

Source code in lutris/installer/commands.py
class CommandsMixin:
    """The directives for the `installer:` part of the install script."""

    def __init__(self):
        if isinstance(self, CommandsMixin):
            raise RuntimeError("This class is a mixin")

    def _get_runner_version(self):
        """Return the version of the runner used for the installer"""
        if self.installer.runner == "wine":
            # If a version is specified in the script choose this one
            if self.installer.script.get(self.installer.runner):
                return self.installer.script[self.installer.runner].get("version")
            # If the installer is a extension, use the wine version from the base game
            if self.installer.requires:
                db_game = get_game_by_field(self.installer.requires, field="installer_slug")
                if not db_game:
                    db_game = get_game_by_field(self.installer.requires, field="slug")
                if not db_game:
                    logger.warning("Can't find game %s", self.installer.requires)
                    return None
                game = Game(db_game["id"])
                return game.config.runner_config["version"]
        if self.installer.runner == "libretro":
            return self.installer.script["game"]["core"]
        return None

    @staticmethod
    def _check_required_params(params, command_data, command_name):
        """Verify presence of a list of parameters required by a command."""
        if isinstance(params, str):
            params = [params]
        for param in params:
            if isinstance(param, tuple):
                param_present = False
                for key in param:
                    if key in command_data:
                        param_present = True
                if not param_present:
                    raise ScriptingError(
                        _("One of {params} parameter is mandatory for the {cmd} command").format(
                            params=_(" or ").join(param), cmd=command_name),
                        command_data,
                    )
            else:
                if param not in command_data:
                    raise ScriptingError(
                        _("The {param} parameter is mandatory for the {cmd} command").format(
                            param=param, cmd=command_name),
                        command_data,
                    )

    @staticmethod
    def _is_cached_file(file_path):
        """Return whether a file referenced by file_id is stored in the cache"""
        pga_cache_path = get_cache_path()
        if not pga_cache_path:
            return False
        return file_path.startswith(pga_cache_path)

    def chmodx(self, filename):
        """Make filename executable"""
        filename = self._substitute(filename)
        if not system.path_exists(filename):
            raise ScriptingError(_("Invalid file '%s'. Can't make it executable") % filename)
        system.make_executable(filename)

    def execute(self, data):
        """Run an executable file."""
        args = []
        terminal = None
        working_dir = None
        env = {}
        if isinstance(data, dict):
            self._check_required_params([("file", "command")], data, "execute")
            if "command" in data and "file" in data:
                raise ScriptingError(
                    _("Parameters file and command can't be used "
                      "at the same time for the execute command"),
                    data,
                )

            # Accept return codes other than 0
            if "return_code" in data:
                return_code = data.pop("return_code")
            else:
                return_code = "0"

            exec_path = data.get("file", "")
            command = data.get("command", "")
            args_string = data.get("args", "")
            for arg in shlex.split(args_string):
                args.append(self._substitute(arg))
            terminal = data.get("terminal")
            working_dir = data.get("working_dir")
            if not data.get("disable_runtime"):
                # Possibly need to handle prefer_system_libs here
                env.update(runtime.get_env())

            # Loading environment variables set in the script
            env.update(self.script_env)

            # Environment variables can also be passed to the execute command
            local_env = data.get("env") or {}
            env.update({key: self._substitute(value) for key, value in local_env.items()})
            include_processes = shlex.split(data.get("include_processes", ""))
            exclude_processes = shlex.split(data.get("exclude_processes", ""))
        elif isinstance(data, str):
            command = data
            include_processes = []
            exclude_processes = []
        else:
            raise ScriptingError(_("No parameters supplied to execute command."), data)

        if command:
            exec_path = "bash"
            args = ["-c", self._get_file_path(command.strip())]
            include_processes.append("bash")
        else:
            # Determine whether 'file' value is a file id or a path
            exec_path = self._get_file_path(exec_path)
        if system.path_exists(exec_path) and not system.is_executable(exec_path):
            logger.warning("Making %s executable", exec_path)
            system.make_executable(exec_path)
        exec_abs_path = system.find_executable(exec_path)
        if not exec_abs_path:
            raise ScriptingError(_("Unable to find executable %s") % exec_path)

        if terminal:
            terminal = linux.get_default_terminal()

        if not working_dir or not os.path.exists(working_dir):
            working_dir = self.target_path

        command = MonitoredCommand(
            [exec_abs_path] + args,
            env=env,
            term=terminal,
            cwd=working_dir,
            include_processes=include_processes,
            exclude_processes=exclude_processes,
        )
        command.accepted_return_code = return_code
        command.start()
        GLib.idle_add(self.parent.attach_logger, command)
        self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
        return "STOP"

    def extract(self, data):
        """Extract a file, guessing the compression method."""
        self._check_required_params([("file", "src")], data, "extract")
        src_param = data.get("file") or data.get("src")
        filespec = self._get_file_path(src_param)

        if os.path.exists(filespec):
            filenames = [filespec]
        else:
            filenames = glob.glob(filespec)

        if not filenames:
            raise ScriptingError(_("%s does not exist") % filespec)
        if "dst" in data:
            dest_path = self._substitute(data["dst"])
        else:
            dest_path = self.target_path
        for filename in filenames:
            msg = _("Extracting %s") % os.path.basename(filename)
            logger.debug(msg)
            GLib.idle_add(self.parent.set_status, msg)
            merge_single = "nomerge" not in data
            extractor = data.get("format")
            logger.debug("extracting file %s to %s", filename, dest_path)
            self._killable_process(extract.extract_archive, filename, dest_path, merge_single, extractor)
        logger.debug("Extract done")

    def input_menu(self, data):
        """Display an input request as a dropdown menu with options."""
        self._check_required_params("options", data, "input_menu")
        identifier = data.get("id")
        alias = "INPUT_%s" % identifier if identifier else None
        has_entry = data.get("entry")
        options = data["options"]
        preselect = self._substitute(data.get("preselect", ""))
        GLib.idle_add(
            self.parent.input_menu,
            alias,
            options,
            preselect,
            has_entry,
            self._on_input_menu_validated,
        )
        return "STOP"

    def _on_input_menu_validated(self, _widget, *args):
        alias = args[0]
        menu = args[1]
        choosen_option = menu.get_active_id()
        if choosen_option:
            self.user_inputs.append({"alias": alias, "value": choosen_option})
            GLib.idle_add(self.parent.continue_button.hide)
            self._iter_commands()

    def insert_disc(self, data):
        """Request user to insert an optical disc"""
        self._check_required_params("requires", data, "insert_disc")
        requires = data.get("requires")
        message = data.get(
            "message",
            _("Insert or mount game disc and click Autodetect or\n"
              "use Browse if the disc is mounted on a non standard location."),
        )
        message += (
            _("\n\nLutris is looking for a mounted disk drive or image \n"
              "containing the following file or folder:\n"
              "<i>%s</i>") % requires
        )
        if self.installer.runner == "wine":
            GLib.idle_add(self.parent.eject_button.show)
        GLib.idle_add(self.parent.ask_for_disc, message, self._find_matching_disc, requires)
        return "STOP"

    def _find_matching_disc(self, _widget, requires, extra_path=None):
        if extra_path:
            drives = [extra_path]
        else:
            drives = system.get_mounted_discs()
        for drive in drives:
            required_abspath = os.path.join(drive, requires)
            required_abspath = system.fix_path_case(required_abspath)
            if required_abspath and system.path_exists(required_abspath):
                logger.debug("Found %s on cdrom %s", requires, drive)
                self.game_disc = drive
                self._iter_commands()
                break

    def mkdir(self, directory):
        """Create directory"""
        directory = self._substitute(directory)
        try:
            os.makedirs(directory)
        except OSError:
            logger.debug("Directory %s already exists", directory)
        else:
            logger.debug("Created directory %s", directory)

    def merge(self, params):
        """Merge the contents given by src to destination folder dst"""
        self._check_required_params(["src", "dst"], params, "merge")
        src, dst = self._get_move_paths(params)
        logger.debug("Merging %s into %s", src, dst)
        if not os.path.exists(src):
            if params.get("optional"):
                logger.info("Optional path %s not present", src)
                return
            raise ScriptingError(_("Source does not exist: %s") % src, params)
        os.makedirs(dst, exist_ok=True)
        if os.path.isfile(src):
            # If single file, copy it and change reference in game file so it
            # can be used as executable. Skip copying if the source is the same
            # as destination.
            if os.path.dirname(src) != dst:
                self._killable_process(shutil.copy, src, dst)
            if params["src"] in self.game_files.keys():
                self.game_files[params["src"]] = os.path.join(dst, os.path.basename(src))
            return
        self._killable_process(system.merge_folders, src, dst)

    def copy(self, params):
        """Alias for merge"""
        self.merge(params)

    def move(self, params):
        """Move a file or directory into a destination folder."""
        self._check_required_params(["src", "dst"], params, "move")
        src, dst = self._get_move_paths(params)
        logger.debug("Moving %s to %s", src, dst)
        if not os.path.exists(src):
            if params.get("optional"):
                logger.info("Optional path %s not present", src)
                return
            raise ScriptingError(_("Invalid source for 'move' operation: %s") % src)

        if os.path.isfile(src):
            if os.path.dirname(src) == dst:
                logger.info("Source file is the same as destination, skipping")
                return

            if os.path.exists(os.path.join(dst, os.path.basename(src))):
                # May not be the best choice, but it's the safest.
                # Maybe should display confirmation dialog (Overwrite / Skip) ?
                logger.info("Destination file exists, skipping")
                return
        try:
            if self._is_cached_file(src):
                action = shutil.copy
            else:
                action = shutil.move
            self._killable_process(action, src, dst)
        except shutil.Error as err:
            raise ScriptingError(_("Can't move {src} \nto destination {dst}").format(src=src, dst=dst)) from err

    def rename(self, params):
        """Rename file or folder."""
        self._check_required_params(["src", "dst"], params, "rename")
        src, dst = self._get_move_paths(params)
        if not os.path.exists(src):
            raise ScriptingError(_("Rename error, source path does not exist: %s") % src)
        if os.path.isdir(dst):
            try:
                os.rmdir(dst)  # Remove if empty
            except OSError:
                pass
        if os.path.exists(dst):
            raise ScriptingError(_("Rename error, destination already exists: %s") % src)
        dst_dir = os.path.dirname(dst)

        # Pre-move on dest filesystem to avoid error with
        # os.rename through different filesystems
        temp_dir = os.path.join(dst_dir, "lutris_rename_temp")
        os.makedirs(temp_dir)
        self._killable_process(shutil.move, src, temp_dir)
        src = os.path.join(temp_dir, os.path.basename(src))
        os.renames(src, dst)

    def _get_move_paths(self, params):
        """Process raw 'src' and 'dst' data."""
        try:
            src_ref = params["src"]
        except KeyError as err:
            raise ScriptingError(_("Missing parameter src")) from err
        src = self.game_files.get(src_ref) or self._substitute(src_ref)
        if not src:
            raise ScriptingError(_("Wrong value for 'src' param"), src_ref)
        dst_ref = params["dst"]
        dst = self._substitute(dst_ref)
        if not dst:
            raise ScriptingError(_("Wrong value for 'dst' param"), dst_ref)
        return src.rstrip("/"), dst.rstrip("/")

    def substitute_vars(self, data):
        """Subsitute variable names found in given file."""
        self._check_required_params("file", data, "substitute_vars")
        filename = self._substitute(data["file"])
        logger.debug("Substituting variables for file %s", filename)
        tmp_filename = filename + ".tmp"
        with open(filename, "r", encoding='utf-8') as source_file:
            with open(tmp_filename, "w", encoding='utf-8') as dest_file:
                line = "."
                while line:
                    line = source_file.readline()
                    line = self._substitute(line)
                    dest_file.write(line)
        os.rename(tmp_filename, filename)

    def _get_task_runner_and_name(self, task_name):
        if "." in task_name:
            # Run a task from a different runner
            # than the one for this installer
            runner_name, task_name = task_name.split(".")
        else:
            runner_name = self.installer.runner
        return runner_name, task_name

    def get_wine_path(self):
        """Return absolute path of wine version used during the install"""
        return get_wine_version_exe(self._get_runner_version())

    def task(self, data):
        """Directive triggering another function specific to a runner.

        The 'name' parameter is mandatory. If 'args' is provided it will be
        passed to the runner task.
        """
        self._check_required_params("name", data, "task")
        if self.parent:
            GLib.idle_add(self.parent.cancel_button.set_sensitive, False)
        runner_name, task_name = self._get_task_runner_and_name(data.pop("name"))

        # Accept return codes other than 0
        if "return_code" in data:
            return_code = data.pop("return_code")
        else:
            return_code = "0"

        if runner_name.startswith("wine"):
            wine_path = self.get_wine_path()
            if wine_path:
                data["wine_path"] = wine_path
            data["prefix"] = data.get("prefix") \
                or self.installer.script.get("game", {}).get("prefix") \
                or "$GAMEDIR"
            data["arch"] = data.get("arch") \
                or self.installer.script.get("game", {}).get("arch") \
                or WINE_DEFAULT_ARCH
            if task_name == "wineexec":
                data["env"] = self.script_env

        for key in data:
            value = data[key]
            if isinstance(value, dict):
                for inner_key in value:
                    value[inner_key] = self._substitute(value[inner_key])
            elif isinstance(value, list):
                for index, elem in enumerate(value):
                    value[index] = self._substitute(elem)
            else:
                value = self._substitute(data[key])
            data[key] = value

        task = import_task(runner_name, task_name)
        command = task(**data)
        if command:
            command.accepted_return_code = return_code
        GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
        if isinstance(command, MonitoredCommand):
            # Monitor thread and continue when task has executed
            GLib.idle_add(self.parent.attach_logger, command)
            self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
            return "STOP"
        return None

    def _monitor_task(self, command):
        if not command.is_running:
            logger.debug("Return code: %s", command.return_code)
            if command.return_code not in (command.accepted_return_code, "0"):
                raise ScriptingError(_("Command exited with code %s") % command.return_code)
            self._iter_commands()
            return False
        return True

    def write_file(self, params):
        """Write text to a file."""
        self._check_required_params(["file", "content"], params, "write_file")

        # Get file
        dest_file_path = self._get_file_path(params["file"])

        # Create dir if necessary
        basedir = os.path.dirname(dest_file_path)
        os.makedirs(basedir, exist_ok=True)

        mode = params.get("mode", "w")
        if not mode.startswith(("a", "w")):
            raise ScriptingError(_("Wrong value for write_file mode: '%s'") % mode)

        with open(dest_file_path, mode, encoding='utf-8') as dest_file:
            dest_file.write(self._substitute(params["content"]))

    def write_json(self, params):
        """Write data into a json file."""
        self._check_required_params(["file", "data"], params, "write_json")

        # Get file
        filename = self._get_file_path(params["file"])

        # Create dir if necessary
        basedir = os.path.dirname(filename)
        os.makedirs(basedir, exist_ok=True)

        merge = params.get("merge", True)

        # create an empty file if it doesn't exist
        Path(filename).touch(exist_ok=True)

        with open(filename, "r+" if merge else "w", encoding='utf-8') as json_file:
            json_data = {}
            if merge:
                try:
                    json_data = json.load(json_file)
                except ValueError:
                    logger.error("Failed to parse JSON from file %s", filename)

            json_data = selective_merge(json_data, params.get("data", {}))
            json_file.seek(0)
            json_file.write(json.dumps(json_data, indent=2))

    def write_config(self, params):
        """Write a key-value pair into an INI type config file."""
        if params.get("data"):
            self._check_required_params(["file", "data"], params, "write_config")
        else:
            self._check_required_params(["file", "section", "key", "value"], params, "write_config")
        # Get file
        config_file_path = self._get_file_path(params["file"])

        # Create dir if necessary
        basedir = os.path.dirname(config_file_path)
        os.makedirs(basedir, exist_ok=True)

        merge = params.get("merge", True)

        parser = EvilConfigParser(allow_no_value=True, dict_type=MultiOrderedDict, strict=False)
        parser.optionxform = str  # Preserve text case
        if merge:
            parser.read(config_file_path)

        data = {}
        if params.get("data"):
            data = params["data"]
        else:
            data[params["section"]] = {}
            data[params["section"]][params["key"]] = params["value"]

        for section, keys in data.items():
            if not parser.has_section(section):
                parser.add_section(section)
            for key, value in keys.items():
                value = self._substitute(value)
                parser.set(section, key, value)

        with open(config_file_path, "wb") as config_file:
            parser.write(config_file)

    def _get_file_path(self, fileid):
        file_path = self.game_files.get(fileid)
        if not file_path:
            file_path = self._substitute(fileid)
        return file_path

    def _killable_process(self, func, *args, **kwargs):
        """Run function `func` in a separate, killable process."""
        with multiprocessing.Pool(1) as process:
            result_obj = process.apply_async(func, args, kwargs)
            self.abort_current_task = process.terminate
            result = result_obj.get()  # Wait process end & re-raise exceptions
            self.abort_current_task = None
            logger.debug("Process %s returned: %s", func, result)
            return result

    def _extract_gog_game(self, file_id):
        self.extract({
            "src": file_id,
            "dst": "$GAMEDIR",
            "extractor": "innoextract"
        })
        app_path = os.path.join(self.target_path, "app")
        if system.path_exists(app_path):
            for app_content in os.listdir(app_path):
                source_path = os.path.join(app_path, app_content)
                if os.path.exists(os.path.join(self.target_path, app_content)):
                    self.merge({"src": source_path, "dst": self.target_path})
                else:
                    self.move({"src": source_path, "dst": self.target_path})
        support_path = os.path.join(self.target_path, "__support/app")
        if system.path_exists(support_path):
            self.merge({"src": support_path, "dst": self.target_path})

    def _get_scummvm_arguments(self, gog_config_path):
        """Return a ScummVM configuration from the GOG config files"""
        with open(gog_config_path, encoding='utf-8') as gog_config_file:
            gog_config = json.loads(gog_config_file.read())
        game_tasks = [task for task in gog_config["playTasks"] if task["category"] == "game"]
        arguments = game_tasks[0]["arguments"]
        game_id = arguments.split()[-1]
        arguments = " ".join(arguments.split()[:-1])
        base_dir = os.path.dirname(gog_config_path)
        return {
            "game_id": game_id,
            "path": base_dir,
            "arguments": arguments
        }

    def autosetup_gog_game(self, file_id, silent=False):
        """Automatically guess the best way to install a GOG game by inspecting its contents.
        This chooses the right runner (DOSBox, Wine) for Windows game files.
        Linux setup files don't use innosetup, they can be unzipped instead.
        """
        file_path = self.game_files[file_id]
        file_list = extract.get_innoextract_list(file_path)
        dosbox_found = False
        scummvm_found = False
        windows_override_found = False  # DOS games that also have a Windows executable
        for filename in file_list:
            if "dosbox/dosbox.exe" in filename.lower():
                dosbox_found = True
            if "scummvm/scummvm.exe" in filename.lower():
                scummvm_found = True
            if "_some_windows.exe" in filename.lower():
                # There's not a good way to handle exceptions without extracting the .info file
                # before extracting the game. Added for Quake but GlQuake.exe doesn't run on modern wine
                windows_override_found = True
        if dosbox_found and not windows_override_found:
            self._extract_gog_game(file_id)
            dosbox_config = {
                "working_dir": "$GAMEDIR/DOSBOX",
            }
            for filename in os.listdir(self.target_path):
                if filename.endswith("_single.conf"):
                    dosbox_config["main_file"] = filename
                elif filename.endswith(".conf"):
                    dosbox_config["config_file"] = filename
            self.installer.script["game"] = dosbox_config
            self.installer.runner = "dosbox"
        elif scummvm_found:
            self._extract_gog_game(file_id)
            arguments = None
            for filename in os.listdir(self.target_path):
                if filename.startswith("goggame") and filename.endswith(".info"):
                    arguments = self._get_scummvm_arguments(os.path.join(self.target_path, filename))
            if not arguments:
                raise RuntimeError("Unable to get ScummVM arguments")
            logger.info("ScummVM config: %s", arguments)
            self.installer.script["game"] = arguments
            self.installer.runner = "scummvm"
        else:
            args = "/SP- /NOCANCEL"
            if silent:
                args += " /SUPPRESSMSGBOXES /VERYSILENT /NOGUI"
            self.installer.is_gog = True
            return self.task({
                "name": "wineexec",
                "prefix": "$GAMEDIR",
                "executable": file_id,
                "args": args
            })
__init__(self) special
Source code in lutris/installer/commands.py
def __init__(self):
    if isinstance(self, CommandsMixin):
        raise RuntimeError("This class is a mixin")
autosetup_gog_game(self, file_id, silent=False)

Automatically guess the best way to install a GOG game by inspecting its contents. This chooses the right runner (DOSBox, Wine) for Windows game files. Linux setup files don't use innosetup, they can be unzipped instead.

Source code in lutris/installer/commands.py
def autosetup_gog_game(self, file_id, silent=False):
    """Automatically guess the best way to install a GOG game by inspecting its contents.
    This chooses the right runner (DOSBox, Wine) for Windows game files.
    Linux setup files don't use innosetup, they can be unzipped instead.
    """
    file_path = self.game_files[file_id]
    file_list = extract.get_innoextract_list(file_path)
    dosbox_found = False
    scummvm_found = False
    windows_override_found = False  # DOS games that also have a Windows executable
    for filename in file_list:
        if "dosbox/dosbox.exe" in filename.lower():
            dosbox_found = True
        if "scummvm/scummvm.exe" in filename.lower():
            scummvm_found = True
        if "_some_windows.exe" in filename.lower():
            # There's not a good way to handle exceptions without extracting the .info file
            # before extracting the game. Added for Quake but GlQuake.exe doesn't run on modern wine
            windows_override_found = True
    if dosbox_found and not windows_override_found:
        self._extract_gog_game(file_id)
        dosbox_config = {
            "working_dir": "$GAMEDIR/DOSBOX",
        }
        for filename in os.listdir(self.target_path):
            if filename.endswith("_single.conf"):
                dosbox_config["main_file"] = filename
            elif filename.endswith(".conf"):
                dosbox_config["config_file"] = filename
        self.installer.script["game"] = dosbox_config
        self.installer.runner = "dosbox"
    elif scummvm_found:
        self._extract_gog_game(file_id)
        arguments = None
        for filename in os.listdir(self.target_path):
            if filename.startswith("goggame") and filename.endswith(".info"):
                arguments = self._get_scummvm_arguments(os.path.join(self.target_path, filename))
        if not arguments:
            raise RuntimeError("Unable to get ScummVM arguments")
        logger.info("ScummVM config: %s", arguments)
        self.installer.script["game"] = arguments
        self.installer.runner = "scummvm"
    else:
        args = "/SP- /NOCANCEL"
        if silent:
            args += " /SUPPRESSMSGBOXES /VERYSILENT /NOGUI"
        self.installer.is_gog = True
        return self.task({
            "name": "wineexec",
            "prefix": "$GAMEDIR",
            "executable": file_id,
            "args": args
        })
chmodx(self, filename)

Make filename executable

Source code in lutris/installer/commands.py
def chmodx(self, filename):
    """Make filename executable"""
    filename = self._substitute(filename)
    if not system.path_exists(filename):
        raise ScriptingError(_("Invalid file '%s'. Can't make it executable") % filename)
    system.make_executable(filename)
copy(self, params)

Alias for merge

Source code in lutris/installer/commands.py
def copy(self, params):
    """Alias for merge"""
    self.merge(params)
execute(self, data)

Run an executable file.

Source code in lutris/installer/commands.py
def execute(self, data):
    """Run an executable file."""
    args = []
    terminal = None
    working_dir = None
    env = {}
    if isinstance(data, dict):
        self._check_required_params([("file", "command")], data, "execute")
        if "command" in data and "file" in data:
            raise ScriptingError(
                _("Parameters file and command can't be used "
                  "at the same time for the execute command"),
                data,
            )

        # Accept return codes other than 0
        if "return_code" in data:
            return_code = data.pop("return_code")
        else:
            return_code = "0"

        exec_path = data.get("file", "")
        command = data.get("command", "")
        args_string = data.get("args", "")
        for arg in shlex.split(args_string):
            args.append(self._substitute(arg))
        terminal = data.get("terminal")
        working_dir = data.get("working_dir")
        if not data.get("disable_runtime"):
            # Possibly need to handle prefer_system_libs here
            env.update(runtime.get_env())

        # Loading environment variables set in the script
        env.update(self.script_env)

        # Environment variables can also be passed to the execute command
        local_env = data.get("env") or {}
        env.update({key: self._substitute(value) for key, value in local_env.items()})
        include_processes = shlex.split(data.get("include_processes", ""))
        exclude_processes = shlex.split(data.get("exclude_processes", ""))
    elif isinstance(data, str):
        command = data
        include_processes = []
        exclude_processes = []
    else:
        raise ScriptingError(_("No parameters supplied to execute command."), data)

    if command:
        exec_path = "bash"
        args = ["-c", self._get_file_path(command.strip())]
        include_processes.append("bash")
    else:
        # Determine whether 'file' value is a file id or a path
        exec_path = self._get_file_path(exec_path)
    if system.path_exists(exec_path) and not system.is_executable(exec_path):
        logger.warning("Making %s executable", exec_path)
        system.make_executable(exec_path)
    exec_abs_path = system.find_executable(exec_path)
    if not exec_abs_path:
        raise ScriptingError(_("Unable to find executable %s") % exec_path)

    if terminal:
        terminal = linux.get_default_terminal()

    if not working_dir or not os.path.exists(working_dir):
        working_dir = self.target_path

    command = MonitoredCommand(
        [exec_abs_path] + args,
        env=env,
        term=terminal,
        cwd=working_dir,
        include_processes=include_processes,
        exclude_processes=exclude_processes,
    )
    command.accepted_return_code = return_code
    command.start()
    GLib.idle_add(self.parent.attach_logger, command)
    self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
    return "STOP"
extract(self, data)

Extract a file, guessing the compression method.

Source code in lutris/installer/commands.py
def extract(self, data):
    """Extract a file, guessing the compression method."""
    self._check_required_params([("file", "src")], data, "extract")
    src_param = data.get("file") or data.get("src")
    filespec = self._get_file_path(src_param)

    if os.path.exists(filespec):
        filenames = [filespec]
    else:
        filenames = glob.glob(filespec)

    if not filenames:
        raise ScriptingError(_("%s does not exist") % filespec)
    if "dst" in data:
        dest_path = self._substitute(data["dst"])
    else:
        dest_path = self.target_path
    for filename in filenames:
        msg = _("Extracting %s") % os.path.basename(filename)
        logger.debug(msg)
        GLib.idle_add(self.parent.set_status, msg)
        merge_single = "nomerge" not in data
        extractor = data.get("format")
        logger.debug("extracting file %s to %s", filename, dest_path)
        self._killable_process(extract.extract_archive, filename, dest_path, merge_single, extractor)
    logger.debug("Extract done")
get_wine_path(self)

Return absolute path of wine version used during the install

Source code in lutris/installer/commands.py
def get_wine_path(self):
    """Return absolute path of wine version used during the install"""
    return get_wine_version_exe(self._get_runner_version())
input_menu(self, data)

Display an input request as a dropdown menu with options.

Source code in lutris/installer/commands.py
def input_menu(self, data):
    """Display an input request as a dropdown menu with options."""
    self._check_required_params("options", data, "input_menu")
    identifier = data.get("id")
    alias = "INPUT_%s" % identifier if identifier else None
    has_entry = data.get("entry")
    options = data["options"]
    preselect = self._substitute(data.get("preselect", ""))
    GLib.idle_add(
        self.parent.input_menu,
        alias,
        options,
        preselect,
        has_entry,
        self._on_input_menu_validated,
    )
    return "STOP"
insert_disc(self, data)

Request user to insert an optical disc

Source code in lutris/installer/commands.py
def insert_disc(self, data):
    """Request user to insert an optical disc"""
    self._check_required_params("requires", data, "insert_disc")
    requires = data.get("requires")
    message = data.get(
        "message",
        _("Insert or mount game disc and click Autodetect or\n"
          "use Browse if the disc is mounted on a non standard location."),
    )
    message += (
        _("\n\nLutris is looking for a mounted disk drive or image \n"
          "containing the following file or folder:\n"
          "<i>%s</i>") % requires
    )
    if self.installer.runner == "wine":
        GLib.idle_add(self.parent.eject_button.show)
    GLib.idle_add(self.parent.ask_for_disc, message, self._find_matching_disc, requires)
    return "STOP"
merge(self, params)

Merge the contents given by src to destination folder dst

Source code in lutris/installer/commands.py
def merge(self, params):
    """Merge the contents given by src to destination folder dst"""
    self._check_required_params(["src", "dst"], params, "merge")
    src, dst = self._get_move_paths(params)
    logger.debug("Merging %s into %s", src, dst)
    if not os.path.exists(src):
        if params.get("optional"):
            logger.info("Optional path %s not present", src)
            return
        raise ScriptingError(_("Source does not exist: %s") % src, params)
    os.makedirs(dst, exist_ok=True)
    if os.path.isfile(src):
        # If single file, copy it and change reference in game file so it
        # can be used as executable. Skip copying if the source is the same
        # as destination.
        if os.path.dirname(src) != dst:
            self._killable_process(shutil.copy, src, dst)
        if params["src"] in self.game_files.keys():
            self.game_files[params["src"]] = os.path.join(dst, os.path.basename(src))
        return
    self._killable_process(system.merge_folders, src, dst)
mkdir(self, directory)

Create directory

Source code in lutris/installer/commands.py
def mkdir(self, directory):
    """Create directory"""
    directory = self._substitute(directory)
    try:
        os.makedirs(directory)
    except OSError:
        logger.debug("Directory %s already exists", directory)
    else:
        logger.debug("Created directory %s", directory)
move(self, params)

Move a file or directory into a destination folder.

Source code in lutris/installer/commands.py
def move(self, params):
    """Move a file or directory into a destination folder."""
    self._check_required_params(["src", "dst"], params, "move")
    src, dst = self._get_move_paths(params)
    logger.debug("Moving %s to %s", src, dst)
    if not os.path.exists(src):
        if params.get("optional"):
            logger.info("Optional path %s not present", src)
            return
        raise ScriptingError(_("Invalid source for 'move' operation: %s") % src)

    if os.path.isfile(src):
        if os.path.dirname(src) == dst:
            logger.info("Source file is the same as destination, skipping")
            return

        if os.path.exists(os.path.join(dst, os.path.basename(src))):
            # May not be the best choice, but it's the safest.
            # Maybe should display confirmation dialog (Overwrite / Skip) ?
            logger.info("Destination file exists, skipping")
            return
    try:
        if self._is_cached_file(src):
            action = shutil.copy
        else:
            action = shutil.move
        self._killable_process(action, src, dst)
    except shutil.Error as err:
        raise ScriptingError(_("Can't move {src} \nto destination {dst}").format(src=src, dst=dst)) from err
rename(self, params)

Rename file or folder.

Source code in lutris/installer/commands.py
def rename(self, params):
    """Rename file or folder."""
    self._check_required_params(["src", "dst"], params, "rename")
    src, dst = self._get_move_paths(params)
    if not os.path.exists(src):
        raise ScriptingError(_("Rename error, source path does not exist: %s") % src)
    if os.path.isdir(dst):
        try:
            os.rmdir(dst)  # Remove if empty
        except OSError:
            pass
    if os.path.exists(dst):
        raise ScriptingError(_("Rename error, destination already exists: %s") % src)
    dst_dir = os.path.dirname(dst)

    # Pre-move on dest filesystem to avoid error with
    # os.rename through different filesystems
    temp_dir = os.path.join(dst_dir, "lutris_rename_temp")
    os.makedirs(temp_dir)
    self._killable_process(shutil.move, src, temp_dir)
    src = os.path.join(temp_dir, os.path.basename(src))
    os.renames(src, dst)
substitute_vars(self, data)

Subsitute variable names found in given file.

Source code in lutris/installer/commands.py
def substitute_vars(self, data):
    """Subsitute variable names found in given file."""
    self._check_required_params("file", data, "substitute_vars")
    filename = self._substitute(data["file"])
    logger.debug("Substituting variables for file %s", filename)
    tmp_filename = filename + ".tmp"
    with open(filename, "r", encoding='utf-8') as source_file:
        with open(tmp_filename, "w", encoding='utf-8') as dest_file:
            line = "."
            while line:
                line = source_file.readline()
                line = self._substitute(line)
                dest_file.write(line)
    os.rename(tmp_filename, filename)
task(self, data)

Directive triggering another function specific to a runner.

The 'name' parameter is mandatory. If 'args' is provided it will be passed to the runner task.

Source code in lutris/installer/commands.py
def task(self, data):
    """Directive triggering another function specific to a runner.

    The 'name' parameter is mandatory. If 'args' is provided it will be
    passed to the runner task.
    """
    self._check_required_params("name", data, "task")
    if self.parent:
        GLib.idle_add(self.parent.cancel_button.set_sensitive, False)
    runner_name, task_name = self._get_task_runner_and_name(data.pop("name"))

    # Accept return codes other than 0
    if "return_code" in data:
        return_code = data.pop("return_code")
    else:
        return_code = "0"

    if runner_name.startswith("wine"):
        wine_path = self.get_wine_path()
        if wine_path:
            data["wine_path"] = wine_path
        data["prefix"] = data.get("prefix") \
            or self.installer.script.get("game", {}).get("prefix") \
            or "$GAMEDIR"
        data["arch"] = data.get("arch") \
            or self.installer.script.get("game", {}).get("arch") \
            or WINE_DEFAULT_ARCH
        if task_name == "wineexec":
            data["env"] = self.script_env

    for key in data:
        value = data[key]
        if isinstance(value, dict):
            for inner_key in value:
                value[inner_key] = self._substitute(value[inner_key])
        elif isinstance(value, list):
            for index, elem in enumerate(value):
                value[index] = self._substitute(elem)
        else:
            value = self._substitute(data[key])
        data[key] = value

    task = import_task(runner_name, task_name)
    command = task(**data)
    if command:
        command.accepted_return_code = return_code
    GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
    if isinstance(command, MonitoredCommand):
        # Monitor thread and continue when task has executed
        GLib.idle_add(self.parent.attach_logger, command)
        self.heartbeat = GLib.timeout_add(1000, self._monitor_task, command)
        return "STOP"
    return None
write_config(self, params)

Write a key-value pair into an INI type config file.

Source code in lutris/installer/commands.py
def write_config(self, params):
    """Write a key-value pair into an INI type config file."""
    if params.get("data"):
        self._check_required_params(["file", "data"], params, "write_config")
    else:
        self._check_required_params(["file", "section", "key", "value"], params, "write_config")
    # Get file
    config_file_path = self._get_file_path(params["file"])

    # Create dir if necessary
    basedir = os.path.dirname(config_file_path)
    os.makedirs(basedir, exist_ok=True)

    merge = params.get("merge", True)

    parser = EvilConfigParser(allow_no_value=True, dict_type=MultiOrderedDict, strict=False)
    parser.optionxform = str  # Preserve text case
    if merge:
        parser.read(config_file_path)

    data = {}
    if params.get("data"):
        data = params["data"]
    else:
        data[params["section"]] = {}
        data[params["section"]][params["key"]] = params["value"]

    for section, keys in data.items():
        if not parser.has_section(section):
            parser.add_section(section)
        for key, value in keys.items():
            value = self._substitute(value)
            parser.set(section, key, value)

    with open(config_file_path, "wb") as config_file:
        parser.write(config_file)
write_file(self, params)

Write text to a file.

Source code in lutris/installer/commands.py
def write_file(self, params):
    """Write text to a file."""
    self._check_required_params(["file", "content"], params, "write_file")

    # Get file
    dest_file_path = self._get_file_path(params["file"])

    # Create dir if necessary
    basedir = os.path.dirname(dest_file_path)
    os.makedirs(basedir, exist_ok=True)

    mode = params.get("mode", "w")
    if not mode.startswith(("a", "w")):
        raise ScriptingError(_("Wrong value for write_file mode: '%s'") % mode)

    with open(dest_file_path, mode, encoding='utf-8') as dest_file:
        dest_file.write(self._substitute(params["content"]))
write_json(self, params)

Write data into a json file.

Source code in lutris/installer/commands.py
def write_json(self, params):
    """Write data into a json file."""
    self._check_required_params(["file", "data"], params, "write_json")

    # Get file
    filename = self._get_file_path(params["file"])

    # Create dir if necessary
    basedir = os.path.dirname(filename)
    os.makedirs(basedir, exist_ok=True)

    merge = params.get("merge", True)

    # create an empty file if it doesn't exist
    Path(filename).touch(exist_ok=True)

    with open(filename, "r+" if merge else "w", encoding='utf-8') as json_file:
        json_data = {}
        if merge:
            try:
                json_data = json.load(json_file)
            except ValueError:
                logger.error("Failed to parse JSON from file %s", filename)

        json_data = selective_merge(json_data, params.get("data", {}))
        json_file.seek(0)
        json_file.write(json.dumps(json_data, indent=2))

errors

Installer specific exceptions

FileNotAvailable (Exception)

Raised when a file has to be provided by the user

Source code in lutris/installer/errors.py
class FileNotAvailable(Exception):

    """Raised when a file has to be provided by the user"""

MissingGameDependency (Exception)

Raise when a game requires another game that isn't installed

Source code in lutris/installer/errors.py
class MissingGameDependency(Exception):

    """Raise when a game requires another game that isn't installed"""

    def __init__(self, slug=None):
        self.slug = slug
        super().__init__()
__init__(self, slug=None) special
Source code in lutris/installer/errors.py
def __init__(self, slug=None):
    self.slug = slug
    super().__init__()

ScriptingError (Exception)

Custom exception for scripting errors, can be caught by modifying excepthook.

Source code in lutris/installer/errors.py
class ScriptingError(Exception):

    """Custom exception for scripting errors, can be caught by modifying
    excepthook."""

    def __init__(self, message, faulty_data=None):
        self.message = message
        self.faulty_data = faulty_data
        super().__init__()
        logger.error(self.__str__())

    def __str__(self):
        faulty_data = repr(self.faulty_data)
        return self.message + "\n%s" % faulty_data if faulty_data else ""

    def __repr__(self):
        return self.message
__init__(self, message, faulty_data=None) special
Source code in lutris/installer/errors.py
def __init__(self, message, faulty_data=None):
    self.message = message
    self.faulty_data = faulty_data
    super().__init__()
    logger.error(self.__str__())
__repr__(self) special
Source code in lutris/installer/errors.py
def __repr__(self):
    return self.message
__str__(self) special
Source code in lutris/installer/errors.py
def __str__(self):
    faulty_data = repr(self.faulty_data)
    return self.message + "\n%s" % faulty_data if faulty_data else ""

error_handler(error_type, value, traceback)

Intercept all possible exceptions and raise them as ScriptingErrors

Source code in lutris/installer/errors.py
def error_handler(error_type, value, traceback):
    """Intercept all possible exceptions and raise them as ScriptingErrors"""
    if error_type == ScriptingError:
        message = value.message
        if value.faulty_data:
            message += "\n<b>%s</b>" % gtk_safe(value.faulty_data)
        ErrorDialog(message)
    else:
        _excepthook(error_type, value, traceback)

installer

Lutris installer class

LutrisInstaller

Represents a Lutris installer

Source code in lutris/installer/installer.py
class LutrisInstaller:  # pylint: disable=too-many-instance-attributes
    """Represents a Lutris installer"""

    def __init__(self, installer, interpreter, service, appid):
        self.interpreter = interpreter
        self.installer = installer
        self.is_update = False
        self.version = installer["version"]
        self.slug = installer["slug"]
        self.year = installer.get("year")
        self.runner = installer["runner"]
        self.script = installer.get("script")
        self.game_name = installer["name"]
        self.game_slug = installer["game_slug"]
        self.service = self.get_service(initial=service)
        self.service_appid = self.get_appid(installer, initial=appid)
        self.variables = installer.get("variables", {})
        self.files = [
            InstallerFile(self.game_slug, file_id, file_meta)
            for file_desc in self.script.get("files", [])
            for file_id, file_meta in file_desc.items()
        ]
        self.requires = self.script.get("requires")
        self.extends = self.script.get("extends")
        self.game_id = self.get_game_id()
        self.is_gog = False

    def get_service(self, initial=None):
        if initial:
            return initial
        if "steam" in self.runner and "steam" in SERVICES:
            return SERVICES["steam"]()
        version = self.version.lower()
        if "humble" in version and "humblebundle" in SERVICES:
            return SERVICES["humblebundle"]()
        if "gog" in version and "gog" in SERVICES:
            return SERVICES["gog"]()

    def get_appid(self, installer, initial=None):
        if installer.get("is_dlc"):
            return installer.get("dlcid")
        if initial:
            return initial
        if not self.service:
            return
        if self.service.id == "steam":
            return installer.get("steamid")
        game_config = self.script.get("game", {})
        if self.service.id == "gog":
            return game_config.get("gogid") or installer.get("gogid")
        if self.service.id == "humblebundle":
            return game_config.get("humbleid") or installer.get("humblestoreid")

    @property
    def script_pretty(self):
        """Return a pretty print of the script"""
        return json.dumps(self.script, indent=4)

    def get_game_id(self):
        """Return the ID of the game in the local DB if one exists"""
        # If the game is in the library and uninstalled, the first installation
        # updates it
        existing_game = get_game_by_field(self.game_slug, "slug")
        if existing_game and (self.extends or not existing_game["installed"]):
            return existing_game["id"]

    @property
    def creates_game_folder(self):
        """Determines if an install script should create a game folder for the game"""
        if self.requires or self.extends:
            # Game is an extension of an existing game, folder exists
            return False
        if self.runner == "steam":
            # Steam games installs in their steamapps directory
            return False
        if (
                self.files
                or self.script.get("game", {}).get("gog")
                or self.script.get("game", {}).get("prefix")
        ):
            return True
        command_names = [list(c.keys())[0] for c in self.script.get("installer", [])]
        if "insert-disc" in command_names:
            return True
        return False

    def get_errors(self):
        """Return potential errors in the script"""
        errors = []
        if not isinstance(self.script, dict):
            errors.append("Script must be a dictionary")
            # Return early since the method assumes a dict
            return errors

        # Check that installers contains all required fields
        for field in ("runner", "game_name", "game_slug"):
            if not hasattr(self, field) or not getattr(self, field):
                errors.append("Missing field '%s'" % field)

        # Check that libretro installers have a core specified
        if self.runner == "libretro":
            if "game" not in self.script or "core" not in self.script["game"]:
                errors.append("Missing libretro core in game section")

        # Check that Steam games have an AppID
        if self.runner == "steam":
            if not self.script.get("game", {}).get("appid"):
                errors.append("Missing appid for Steam game")

        # Check that installers don't contain both 'requires' and 'extends'
        if self.script.get("requires") and self.script.get("extends"):
            errors.append("Scripts can't have both extends and requires")
        return errors

    def pop_user_provided_file(self):
        """Return and remove the first user provided file, which is used for game stores"""
        for index, file in enumerate(self.files):
            if file.url.startswith("N/A"):
                self.files.pop(index)
                return file.id

    def prepare_game_files(self, patch_version=None):
        """Gathers necessary files before iterating through them."""
        if not self.files:
            logger.info("No files to prepare")
            return
        if not self.service:
            logger.debug("No service to retrieve files from")
            return
        if self.service.online and not self.service.is_connected():
            logger.info("Not authenticated to %s", self.service.id)
            return
        installer_file_id = self.pop_user_provided_file()
        if not installer_file_id:
            logger.warning("Could not find a file for this service")
            return
        logger.info("Getting files for %s", installer_file_id)
        if self.service.has_extras:
            logger.info("Adding selected extras to downloads")
            self.service.selected_extras = self.interpreter.extras
        if patch_version:
            # If a patch version is given download the patch files instead of the installer
            installer_files = self.service.get_patch_files(self, installer_file_id)
        else:
            installer_files = self.service.get_installer_files(self, installer_file_id)
        for installer_file in installer_files:
            self.files.append(installer_file)
        if not installer_files:
            # Failed to get the service game, put back a user provided file
            logger.debug("Unable to get files from service. Setting %s to manual.", installer_file_id)
            self.files.insert(0, InstallerFile(self.game_slug, installer_file_id, {
                "url": "N/A: Provider installer file",
                "filename": ""
            }))

    def _substitute_config(self, script_config):
        """Substitute values such as $GAMEDIR in a config dict."""
        config = {}
        for key in script_config:
            if not isinstance(key, str):
                raise ScriptingError(_("Game config key must be a string"), key)
            value = script_config[key]
            if str(value).lower() == 'true':
                value = True
            if str(value).lower() == 'false':
                value = False
            if key == "launch_configs":
                # launch configuration don't need substitutions at least for now.
                config[key] = value
            elif isinstance(value, list):
                config[key] = [self.interpreter._substitute(i) for i in value]
            elif isinstance(value, dict):
                config[key] = {k: self.interpreter._substitute(v) for (k, v) in value.items()}
            elif isinstance(value, bool):
                config[key] = value

            else:
                config[key] = self.interpreter._substitute(value)
        return config

    def get_game_config(self):
        """Return the game configuration"""
        if self.requires:
            # Load the base game config
            required_game = get_game_by_field(self.requires, field="installer_slug")
            if not required_game:
                required_game = get_game_by_field(self.requires, field="slug")
            if not required_game:
                raise ValueError("No game matched '%s' on installer_slug or slug" % self.requires)
            base_config = LutrisConfig(
                runner_slug=self.runner, game_config_id=required_game["configpath"]
            )
            config = base_config.game_level
        else:
            config = {"game": {}}

        # Config update
        if "system" in self.script:
            config["system"] = self._substitute_config(self.script["system"])
        if self.runner in self.script and self.script[self.runner]:
            config[self.runner] = self._substitute_config(self.script[self.runner])
        launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)
        if launcher:
            config["game"][launcher] = launcher_config

        if "game" in self.script:
            try:
                config["game"].update(self.script["game"])
            except ValueError as err:
                raise ScriptingError(_("Invalid 'game' section"), self.script["game"]) from err
            config["game"] = self._substitute_config(config["game"])
            if AUTO_ELF_EXE in config["game"].get("exe", ""):
                config["game"]["exe"] = find_linux_game_executable(self.interpreter.target_path,
                                                                   make_executable=True)
            elif AUTO_WIN32_EXE in config["game"].get("exe", ""):
                config["game"]["exe"] = find_windows_game_executable(self.interpreter.target_path)
        return config

    def save(self):
        """Write the game configuration in the DB and config file"""
        if self.extends:
            logger.info(
                "This is an extension to %s, not creating a new game entry",
                self.extends,
            )
            return self.game_id

        if self.is_gog:
            gog_config = get_gog_config_from_path(self.interpreter.target_path)
            if gog_config:
                gog_game_path = get_gog_game_path(self.interpreter.target_path)
                lutris_config = convert_gog_config_to_lutris(gog_config, gog_game_path)
                self.script["game"].update(lutris_config)

        configpath = write_game_config(self.slug, self.get_game_config())
        runner_inst = import_runner(self.runner)()
        if self.service:
            service_id = self.service.id
        else:
            service_id = None
        self.game_id = add_or_update(
            name=self.game_name,
            runner=self.runner,
            slug=self.game_slug,
            platform=runner_inst.get_platform(),
            directory=self.interpreter.target_path,
            installed=1,
            hidden=0,
            installer_slug=self.slug,
            parent_slug=self.requires,
            year=self.year,
            configpath=configpath,
            service=service_id,
            service_id=self.service_appid,
            id=self.game_id,
        )
        return self.game_id

    def get_game_launcher_config(self, game_files):
        """Game options such as exe or main_file can be added at the root of the
        script as a shortcut, this integrates them into the game config properly
        This should be deprecated. Game launchers should go in the game section.
        """
        launcher, launcher_value = get_game_launcher(self.script)
        if isinstance(launcher_value, list):
            launcher_values = []
            for game_file in launcher_value:
                if game_file in game_files:
                    launcher_values.append(game_files[game_file])
                else:
                    launcher_values.append(game_file)
            return launcher, launcher_values
        if launcher_value:
            if launcher_value in game_files:
                launcher_value = game_files[launcher_value]
            elif self.interpreter.target_path and os.path.exists(
                    os.path.join(self.interpreter.target_path, launcher_value)
            ):
                launcher_value = os.path.join(self.interpreter.target_path, launcher_value)
        return launcher, launcher_value
creates_game_folder property readonly

Determines if an install script should create a game folder for the game

script_pretty property readonly

Return a pretty print of the script

__init__(self, installer, interpreter, service, appid) special
Source code in lutris/installer/installer.py
def __init__(self, installer, interpreter, service, appid):
    self.interpreter = interpreter
    self.installer = installer
    self.is_update = False
    self.version = installer["version"]
    self.slug = installer["slug"]
    self.year = installer.get("year")
    self.runner = installer["runner"]
    self.script = installer.get("script")
    self.game_name = installer["name"]
    self.game_slug = installer["game_slug"]
    self.service = self.get_service(initial=service)
    self.service_appid = self.get_appid(installer, initial=appid)
    self.variables = installer.get("variables", {})
    self.files = [
        InstallerFile(self.game_slug, file_id, file_meta)
        for file_desc in self.script.get("files", [])
        for file_id, file_meta in file_desc.items()
    ]
    self.requires = self.script.get("requires")
    self.extends = self.script.get("extends")
    self.game_id = self.get_game_id()
    self.is_gog = False
get_appid(self, installer, initial=None)
Source code in lutris/installer/installer.py
def get_appid(self, installer, initial=None):
    if installer.get("is_dlc"):
        return installer.get("dlcid")
    if initial:
        return initial
    if not self.service:
        return
    if self.service.id == "steam":
        return installer.get("steamid")
    game_config = self.script.get("game", {})
    if self.service.id == "gog":
        return game_config.get("gogid") or installer.get("gogid")
    if self.service.id == "humblebundle":
        return game_config.get("humbleid") or installer.get("humblestoreid")
get_errors(self)

Return potential errors in the script

Source code in lutris/installer/installer.py
def get_errors(self):
    """Return potential errors in the script"""
    errors = []
    if not isinstance(self.script, dict):
        errors.append("Script must be a dictionary")
        # Return early since the method assumes a dict
        return errors

    # Check that installers contains all required fields
    for field in ("runner", "game_name", "game_slug"):
        if not hasattr(self, field) or not getattr(self, field):
            errors.append("Missing field '%s'" % field)

    # Check that libretro installers have a core specified
    if self.runner == "libretro":
        if "game" not in self.script or "core" not in self.script["game"]:
            errors.append("Missing libretro core in game section")

    # Check that Steam games have an AppID
    if self.runner == "steam":
        if not self.script.get("game", {}).get("appid"):
            errors.append("Missing appid for Steam game")

    # Check that installers don't contain both 'requires' and 'extends'
    if self.script.get("requires") and self.script.get("extends"):
        errors.append("Scripts can't have both extends and requires")
    return errors
get_game_config(self)

Return the game configuration

Source code in lutris/installer/installer.py
def get_game_config(self):
    """Return the game configuration"""
    if self.requires:
        # Load the base game config
        required_game = get_game_by_field(self.requires, field="installer_slug")
        if not required_game:
            required_game = get_game_by_field(self.requires, field="slug")
        if not required_game:
            raise ValueError("No game matched '%s' on installer_slug or slug" % self.requires)
        base_config = LutrisConfig(
            runner_slug=self.runner, game_config_id=required_game["configpath"]
        )
        config = base_config.game_level
    else:
        config = {"game": {}}

    # Config update
    if "system" in self.script:
        config["system"] = self._substitute_config(self.script["system"])
    if self.runner in self.script and self.script[self.runner]:
        config[self.runner] = self._substitute_config(self.script[self.runner])
    launcher, launcher_config = self.get_game_launcher_config(self.interpreter.game_files)
    if launcher:
        config["game"][launcher] = launcher_config

    if "game" in self.script:
        try:
            config["game"].update(self.script["game"])
        except ValueError as err:
            raise ScriptingError(_("Invalid 'game' section"), self.script["game"]) from err
        config["game"] = self._substitute_config(config["game"])
        if AUTO_ELF_EXE in config["game"].get("exe", ""):
            config["game"]["exe"] = find_linux_game_executable(self.interpreter.target_path,
                                                               make_executable=True)
        elif AUTO_WIN32_EXE in config["game"].get("exe", ""):
            config["game"]["exe"] = find_windows_game_executable(self.interpreter.target_path)
    return config
get_game_id(self)

Return the ID of the game in the local DB if one exists

Source code in lutris/installer/installer.py
def get_game_id(self):
    """Return the ID of the game in the local DB if one exists"""
    # If the game is in the library and uninstalled, the first installation
    # updates it
    existing_game = get_game_by_field(self.game_slug, "slug")
    if existing_game and (self.extends or not existing_game["installed"]):
        return existing_game["id"]
get_game_launcher_config(self, game_files)

Game options such as exe or main_file can be added at the root of the script as a shortcut, this integrates them into the game config properly This should be deprecated. Game launchers should go in the game section.

Source code in lutris/installer/installer.py
def get_game_launcher_config(self, game_files):
    """Game options such as exe or main_file can be added at the root of the
    script as a shortcut, this integrates them into the game config properly
    This should be deprecated. Game launchers should go in the game section.
    """
    launcher, launcher_value = get_game_launcher(self.script)
    if isinstance(launcher_value, list):
        launcher_values = []
        for game_file in launcher_value:
            if game_file in game_files:
                launcher_values.append(game_files[game_file])
            else:
                launcher_values.append(game_file)
        return launcher, launcher_values
    if launcher_value:
        if launcher_value in game_files:
            launcher_value = game_files[launcher_value]
        elif self.interpreter.target_path and os.path.exists(
                os.path.join(self.interpreter.target_path, launcher_value)
        ):
            launcher_value = os.path.join(self.interpreter.target_path, launcher_value)
    return launcher, launcher_value
get_service(self, initial=None)
Source code in lutris/installer/installer.py
def get_service(self, initial=None):
    if initial:
        return initial
    if "steam" in self.runner and "steam" in SERVICES:
        return SERVICES["steam"]()
    version = self.version.lower()
    if "humble" in version and "humblebundle" in SERVICES:
        return SERVICES["humblebundle"]()
    if "gog" in version and "gog" in SERVICES:
        return SERVICES["gog"]()
pop_user_provided_file(self)

Return and remove the first user provided file, which is used for game stores

Source code in lutris/installer/installer.py
def pop_user_provided_file(self):
    """Return and remove the first user provided file, which is used for game stores"""
    for index, file in enumerate(self.files):
        if file.url.startswith("N/A"):
            self.files.pop(index)
            return file.id
prepare_game_files(self, patch_version=None)

Gathers necessary files before iterating through them.

Source code in lutris/installer/installer.py
def prepare_game_files(self, patch_version=None):
    """Gathers necessary files before iterating through them."""
    if not self.files:
        logger.info("No files to prepare")
        return
    if not self.service:
        logger.debug("No service to retrieve files from")
        return
    if self.service.online and not self.service.is_connected():
        logger.info("Not authenticated to %s", self.service.id)
        return
    installer_file_id = self.pop_user_provided_file()
    if not installer_file_id:
        logger.warning("Could not find a file for this service")
        return
    logger.info("Getting files for %s", installer_file_id)
    if self.service.has_extras:
        logger.info("Adding selected extras to downloads")
        self.service.selected_extras = self.interpreter.extras
    if patch_version:
        # If a patch version is given download the patch files instead of the installer
        installer_files = self.service.get_patch_files(self, installer_file_id)
    else:
        installer_files = self.service.get_installer_files(self, installer_file_id)
    for installer_file in installer_files:
        self.files.append(installer_file)
    if not installer_files:
        # Failed to get the service game, put back a user provided file
        logger.debug("Unable to get files from service. Setting %s to manual.", installer_file_id)
        self.files.insert(0, InstallerFile(self.game_slug, installer_file_id, {
            "url": "N/A: Provider installer file",
            "filename": ""
        }))
save(self)

Write the game configuration in the DB and config file

Source code in lutris/installer/installer.py
def save(self):
    """Write the game configuration in the DB and config file"""
    if self.extends:
        logger.info(
            "This is an extension to %s, not creating a new game entry",
            self.extends,
        )
        return self.game_id

    if self.is_gog:
        gog_config = get_gog_config_from_path(self.interpreter.target_path)
        if gog_config:
            gog_game_path = get_gog_game_path(self.interpreter.target_path)
            lutris_config = convert_gog_config_to_lutris(gog_config, gog_game_path)
            self.script["game"].update(lutris_config)

    configpath = write_game_config(self.slug, self.get_game_config())
    runner_inst = import_runner(self.runner)()
    if self.service:
        service_id = self.service.id
    else:
        service_id = None
    self.game_id = add_or_update(
        name=self.game_name,
        runner=self.runner,
        slug=self.game_slug,
        platform=runner_inst.get_platform(),
        directory=self.interpreter.target_path,
        installed=1,
        hidden=0,
        installer_slug=self.slug,
        parent_slug=self.requires,
        year=self.year,
        configpath=configpath,
        service=service_id,
        service_id=self.service_appid,
        id=self.game_id,
    )
    return self.game_id

installer_file

Manipulates installer files

InstallerFile

Representation of a file in the files sections of an installer

Source code in lutris/installer/installer_file.py
class InstallerFile:
    """Representation of a file in the `files` sections of an installer"""

    def __init__(self, game_slug, file_id, file_meta):
        self.game_slug = game_slug
        self.id = file_id.replace("-", "_")  # pylint: disable=invalid-name
        self._file_meta = file_meta
        self._dest_file = None  # Used to override the destination

    @property
    def url(self):
        _url = ""
        if isinstance(self._file_meta, dict):
            if "url" not in self._file_meta:
                raise ScriptingError(_("missing field `url` for file `%s`") % self.id)
            _url = self._file_meta["url"]
        else:
            _url = self._file_meta
        if _url.startswith("/"):
            return "file://" + _url
        return _url

    @property
    def filename(self):
        if isinstance(self._file_meta, dict):
            if "filename" not in self._file_meta:
                raise ScriptingError(_("missing field `filename` in file `%s`") % self.id)
            return self._file_meta["filename"]
        if self._file_meta.startswith("N/A"):
            if self.uses_pga_cache() and os.path.isdir(self.cache_path):
                return self.cached_filename
            return ""
        if self.url.startswith("$STEAM"):
            return self.url
        return os.path.basename(self._file_meta)

    @property
    def referer(self):
        if isinstance(self._file_meta, dict):
            return self._file_meta.get("referer")

    @property
    def checksum(self):
        if isinstance(self._file_meta, dict):
            return self._file_meta.get("checksum")

    @property
    def dest_file(self):
        if self._dest_file:
            return self._dest_file
        return os.path.join(self.cache_path, self.filename)

    @dest_file.setter
    def dest_file(self, value):
        self._dest_file = value

    def __str__(self):
        return "%s/%s" % (self.game_slug, self.id)

    @property
    def human_url(self):
        """Return the url in human readable format"""
        if self.url.startswith("N/A"):
            # Ask the user where the file is located
            parts = self.url.split(":", 1)
            if len(parts) == 2:
                return parts[1]
            return "Please select file '%s'" % self.id
        return self.url

    @property
    def cached_filename(self):
        """Return the filename of the first file in the cache path"""
        cache_files = os.listdir(self.cache_path)
        if cache_files:
            return cache_files[0]
        return ""

    @property
    def provider(self):
        """Return file provider used"""
        if self.url.startswith("$STEAM"):
            return "steam"
        if self.is_cached:
            return "pga"
        if self.url.startswith("N/A"):
            return "user"
        if self.is_downloadable():
            return "download"
        raise ValueError("Unsupported provider for %s" % self.url)

    @property
    def providers(self):
        """Return all supported providers"""
        _providers = set()
        if self.url.startswith("$STEAM"):
            _providers.add("steam")
        if self.is_cached:
            _providers.add("pga")
        if self.url.startswith("N/A"):
            _providers.add("user")
        if self.is_downloadable():
            _providers.add("download")
        return _providers

    def is_downloadable(self):
        """Return True if the file can be downloaded (even from the local filesystem)"""
        return self.url.startswith(("http", "file"))

    def uses_pga_cache(self, create=False):
        """Determines whether the installer files are stored in a PGA cache

        Params:
            create (bool): If a cache is active, auto create directories if needed
        Returns:
            bool
        """
        cache_path = cache.get_cache_path()
        if not cache_path:
            return False
        if system.path_exists(cache_path):
            return True
        if create:
            try:
                logger.debug("Creating cache path %s", self.cache_path)
                os.makedirs(self.cache_path)
            except (OSError, PermissionError) as ex:
                logger.error("Failed to created cache path: %s", ex)
                return False
            return True
        logger.warning("Cache path %s does not exist", cache_path)
        return False

    @property
    def cache_path(self):
        """Return the directory used as a cache for the duration of the installation"""
        _cache_path = cache.get_cache_path()
        if not _cache_path:
            _cache_path = os.path.join(settings.CACHE_DIR, "installer")
        url_parts = urlparse(self.url)
        if url_parts.netloc.endswith("gog.com"):
            folder = "gog"
        else:
            folder = self.id
        return os.path.join(_cache_path, self.game_slug, folder)

    def prepare(self):
        """Prepare the file for download"""
        if not system.path_exists(self.cache_path):
            os.makedirs(self.cache_path)

    def check_hash(self):
        """Checks the checksum of `file` and compare it to `value`

        Args:
            checksum (str): The checksum to look for (type:hash)
            dest_file (str): The path to the destination file
            dest_file_uri (str): The uri for the destination file
        """
        if not self.checksum or not self.dest_file:
            return
        try:
            hash_type, expected_hash = self.checksum.split(':', 1)
        except ValueError as err:
            raise ScriptingError(_("Invalid checksum, expected format (type:hash) "), self.checksum) from err

        if system.get_file_checksum(self.dest_file, hash_type) != expected_hash:
            raise ScriptingError(hash_type.capitalize() + _(" checksum mismatch "), self.checksum)

    @property
    def is_cached(self):
        """Is the file available in the local PGA cache?"""
        return self.uses_pga_cache() and system.path_exists(self.dest_file)
cache_path property readonly

Return the directory used as a cache for the duration of the installation

cached_filename property readonly

Return the filename of the first file in the cache path

checksum property readonly
dest_file property writable
filename property readonly
human_url property readonly

Return the url in human readable format

is_cached property readonly

Is the file available in the local PGA cache?

provider property readonly

Return file provider used

providers property readonly

Return all supported providers

referer property readonly
url property readonly
__init__(self, game_slug, file_id, file_meta) special
Source code in lutris/installer/installer_file.py
def __init__(self, game_slug, file_id, file_meta):
    self.game_slug = game_slug
    self.id = file_id.replace("-", "_")  # pylint: disable=invalid-name
    self._file_meta = file_meta
    self._dest_file = None  # Used to override the destination
__str__(self) special
Source code in lutris/installer/installer_file.py
def __str__(self):
    return "%s/%s" % (self.game_slug, self.id)
check_hash(self)

Checks the checksum of file and compare it to value

Parameters:

Name Type Description Default
checksum str

The checksum to look for (type:hash)

required
dest_file str

The path to the destination file

required
dest_file_uri str

The uri for the destination file

required
Source code in lutris/installer/installer_file.py
def check_hash(self):
    """Checks the checksum of `file` and compare it to `value`

    Args:
        checksum (str): The checksum to look for (type:hash)
        dest_file (str): The path to the destination file
        dest_file_uri (str): The uri for the destination file
    """
    if not self.checksum or not self.dest_file:
        return
    try:
        hash_type, expected_hash = self.checksum.split(':', 1)
    except ValueError as err:
        raise ScriptingError(_("Invalid checksum, expected format (type:hash) "), self.checksum) from err

    if system.get_file_checksum(self.dest_file, hash_type) != expected_hash:
        raise ScriptingError(hash_type.capitalize() + _(" checksum mismatch "), self.checksum)
is_downloadable(self)

Return True if the file can be downloaded (even from the local filesystem)

Source code in lutris/installer/installer_file.py
def is_downloadable(self):
    """Return True if the file can be downloaded (even from the local filesystem)"""
    return self.url.startswith(("http", "file"))
prepare(self)

Prepare the file for download

Source code in lutris/installer/installer_file.py
def prepare(self):
    """Prepare the file for download"""
    if not system.path_exists(self.cache_path):
        os.makedirs(self.cache_path)
uses_pga_cache(self, create=False)

Determines whether the installer files are stored in a PGA cache

Parameters:

Name Type Description Default
create bool

If a cache is active, auto create directories if needed

False

Returns:

Type Description

bool

Source code in lutris/installer/installer_file.py
def uses_pga_cache(self, create=False):
    """Determines whether the installer files are stored in a PGA cache

    Params:
        create (bool): If a cache is active, auto create directories if needed
    Returns:
        bool
    """
    cache_path = cache.get_cache_path()
    if not cache_path:
        return False
    if system.path_exists(cache_path):
        return True
    if create:
        try:
            logger.debug("Creating cache path %s", self.cache_path)
            os.makedirs(self.cache_path)
        except (OSError, PermissionError) as ex:
            logger.error("Failed to created cache path: %s", ex)
            return False
        return True
    logger.warning("Cache path %s does not exist", cache_path)
    return False

interpreter

Install a game by following its install script.

ScriptInterpreter (Object, CommandsMixin)

Control the execution of an installer

Source code in lutris/installer/interpreter.py
class ScriptInterpreter(GObject.Object, CommandsMixin):
    """Control the execution of an installer"""

    __gsignals__ = {
        "runners-installed": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    def __init__(self, installer, parent=None):
        super().__init__()
        self.target_path = None
        self.parent = parent
        self.service = parent.service if parent else None
        _appid = parent.appid if parent else None
        self.game_dir_created = False  # Whether a game folder was created during the install
        # Extra files for installers, either None if the extras haven't been checked yet.
        # Or a list of IDs of extras to be downloaded during the install
        self.extras = None
        self.game_disc = None
        self.game_files = {}
        self.cancelled = False
        self.abort_current_task = None
        self.user_inputs = []
        self.current_command = 0  # Current installer command when iterating through them
        self.runners_to_install = []
        self.installer = LutrisInstaller(installer, self, service=self.service, appid=_appid)
        if not self.installer.script:
            raise ScriptingError(_("This installer doesn't have a 'script' section"))
        script_errors = self.installer.get_errors()
        if script_errors:
            raise ScriptingError(
                _("Invalid script: \n{}").format("\n".join(script_errors)), self.installer.script
            )

        self.current_resolution = DISPLAY_MANAGER.get_current_resolution()
        self._check_binary_dependencies()
        self._check_dependency()
        if self.installer.creates_game_folder:
            self.target_path = self.get_default_target()

    @property
    def appid(self):
        logger.warning("Do not access appid from interpreter")
        return self.installer.service_appid

    def get_default_target(self):
        """Return default installation dir"""
        config = LutrisConfig(runner_slug=self.installer.runner)
        games_dir = config.system_config.get("game_path", os.path.expanduser("~"))
        if self.service:
            service_dir = self.service.id
        else:
            service_dir = ""
        return os.path.expanduser(os.path.join(games_dir, service_dir, self.installer.game_slug))

    @property
    def cache_path(self):
        """Return the directory used as a cache for the duration of the installation"""
        return os.path.join(settings.CACHE_DIR, "installer/%s" % self.installer.game_slug)

    @property
    def script_env(self):
        """Return the script's own environment variable with values
        susbtituted. This value can be used to provide the same environment
        variable as set for the game during the install process.
        """
        return {
            key: self._substitute(value) for key, value in
            self.installer.script.get('system', {}).get('env', {}).items()
        }

    @staticmethod
    def _get_game_dependency(dependency):
        """Return a game database row from a dependency name"""
        game = get_game_by_field(dependency, field="installer_slug")
        if not game:
            game = get_game_by_field(dependency, "slug")

        # Game must be installed and have a directory
        # set so we can use that as the destination
        if game and game["installed"] and game["directory"]:
            return game

    def _check_binary_dependencies(self):
        """Check if all required binaries are installed on the system.

        This reads a `require-binaries` entry in the script, parsed the same way as
        the `requires` entry.
        """
        binary_dependencies = unpack_dependencies(self.installer.script.get("require-binaries"))
        for dependency in binary_dependencies:
            if isinstance(dependency, tuple):
                installed_binaries = {
                    dependency_option: bool(system.find_executable(dependency_option))
                    for dependency_option in dependency
                }
                if not any(installed_binaries.values()):
                    raise ScriptingError(_("This installer requires %s on your system") % _(" or ").join(dependency))
            else:
                if not system.find_executable(dependency):
                    raise ScriptingError(_("This installer requires %s on your system") % dependency)

    def _check_dependency(self):
        """When a game is a mod or an extension of another game, check that the base
        game is installed.
        If the game is available, install the game in the base game folder.
        The first game available listed in the dependencies is the one picked to base
        the installed on.
        """
        if self.installer.extends:
            dependencies = [self.installer.extends]
        else:
            dependencies = unpack_dependencies(self.installer.requires)
        error_message = _("You need to install {} before")
        for index, dependency in enumerate(dependencies):
            if isinstance(dependency, tuple):
                installed_games = [dep for dep in [self._get_game_dependency(dep) for dep in dependency] if dep]
                if not installed_games:
                    if len(dependency) == 1:
                        raise MissingGameDependency(slug=dependency)
                    raise ScriptingError(error_message.format(_(" or ").join(dependency)))
                if index == 0:
                    self.target_path = installed_games[0]["directory"]
                    self.requires = installed_games[0]["installer_slug"]
            else:
                game = self._get_game_dependency(dependency)
                if not game:
                    raise MissingGameDependency(slug=dependency)
                if index == 0:
                    self.target_path = game["directory"]
                    self.requires = game["installer_slug"]

    def get_extras(self):
        """Get extras and store them to move them at the end of the install"""
        logger.debug("Checking if service provide extra files")
        if not self.service or not self.service.has_extras:
            self.extras = []
            return self.extras
        self.extras = self.service.get_extras(self.installer.service_appid)
        return self.extras

    def launch_install(self):
        """Launch the install process"""
        self.runners_to_install = self.get_runners_to_install()
        self.install_runners()
        self.create_game_folder()

    def create_game_folder(self):
        """Create the game folder if needed and store if is was created"""
        if (
                self.installer.files
                and self.target_path
                and not system.path_exists(self.target_path)
                and self.installer.creates_game_folder
        ):
            try:
                logger.debug("Creating destination path %s", self.target_path)
                os.makedirs(self.target_path)
                self.game_dir_created = True
            except PermissionError as err:
                raise ScriptingError(
                    _("Lutris does not have the necessary permissions to install to path:"),
                    self.target_path,
                ) from err

    def get_runners_to_install(self):
        """Check if the runner is installed before starting the installation
        Install the required runner(s) if necessary. This should handle runner
        dependencies or runners used for installer tasks.
        """
        runners_to_install = []
        required_runners = []
        runner = self.get_runner_class(self.installer.runner)
        required_runners.append(runner())

        for command in self.installer.script.get("installer", []):
            command_name, command_params = self._get_command_name_and_params(command)
            if command_name == "task":
                runner_name, _task_name = self._get_task_runner_and_name(command_params["name"])
                runner_names = [r.name for r in required_runners]
                if runner_name not in runner_names:
                    required_runners.append(self.get_runner_class(runner_name)())

        for runner in required_runners:
            params = {}
            if self.installer.runner == "libretro":
                params["core"] = self.installer.script["game"]["core"]
            if self.installer.runner.startswith("wine"):
                # Force the wine version to be installed
                params["fallback"] = False
                params["min_version"] = wine.MIN_SAFE_VERSION
                version = self._get_runner_version()
                if version:
                    params["version"] = version
                else:
                    # Looking up default wine version
                    default_wine = runner.get_runner_version() or {}
                    if "version" in default_wine:
                        logger.debug("Default wine version is %s", default_wine["version"])
                        # Set the version to both the is_installed params and
                        # the script itself so the version gets saved at the
                        # end of the install.
                        if self.installer.runner not in self.installer.script:
                            self.installer.script[self.installer.runner] = {}
                        version = "{}-{}".format(default_wine["version"],
                                                 default_wine["architecture"])
                        params["version"] = \
                            self.installer.script[self.installer.runner]["version"] = version
                    else:
                        logger.error("Failed to get default wine version (got %s)", default_wine)

            if not runner.is_installed(**params):
                logger.info("Runner %s needs to be installed", runner)
                runners_to_install.append(runner)

        if self.installer.runner.startswith("wine") and not get_wine_version():
            WineNotInstalledWarning(parent=self.parent)
        return runners_to_install

    def install_runners(self):
        """Install required runners for a game"""
        if self.runners_to_install:
            self.install_runner(self.runners_to_install.pop(0))
            return
        self.emit("runners-installed")

    def install_runner(self, runner):
        """Install runner required by the install script"""
        logger.debug("Installing %s", runner.name)
        try:
            runner.install(
                version=self._get_runner_version(),
                downloader=simple_downloader,
                callback=self.install_runners,
            )
        except (NonInstallableRunnerError, RunnerInstallationError) as ex:
            logger.error(ex.message)
            raise ScriptingError(ex.message) from ex

    def get_runner_class(self, runner_name):
        """Runner the runner class from its name"""
        try:
            runner = import_runner(runner_name)
        except InvalidRunner as err:
            GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
            raise ScriptingError(_("Invalid runner provided %s") % runner_name) from err
        return runner

    def launch_installer_commands(self):
        """Run the pre-installation steps and launch install."""
        if self.target_path and os.path.exists(self.target_path):
            os.chdir(self.target_path)
        os.makedirs(self.cache_path, exist_ok=True)

        # Copy extras to game folder
        if len(self.extras) == len(self.installer.files):
            # Reset the install script in case there are only extras.
            logger.warning("Installer with only extras and no game files")
            self.installer.script["installer"] = []

        for extra in self.extras:
            self.installer.script["installer"].append(
                {"copy": {"src": extra, "dst": "$GAMEDIR/extras"}}
            )
        self._iter_commands()

    def _iter_commands(self, result=None, exception=None):

        if result == "STOP" or self.cancelled:
            return

        self.parent.set_status(_("Installing game data"))
        self.parent.add_spinner()
        self.parent.continue_button.hide()

        commands = self.installer.script.get("installer", [])
        if exception:
            logger.error("Last install command failed, show error")
            self.parent.on_install_error(repr(exception))
        elif self.current_command < len(commands):
            try:
                command = commands[self.current_command]
            except KeyError as err:
                raise ScriptingError(_("Installer commands are not formatted correctly")) from err
            self.current_command += 1
            method, params = self._map_command(command)
            if isinstance(params, dict):
                status_text = params.pop("description", None)
            else:
                status_text = None
            if status_text:
                self.parent.set_status(status_text)
            logger.debug("Installer command: %s", command)
            AsyncCall(method, self._iter_commands, params)
        else:
            self._finish_install()

    @staticmethod
    def _get_command_name_and_params(command_data):
        if isinstance(command_data, dict):
            command_name = list(command_data.keys())[0]
            command_params = command_data[command_name]
        else:
            command_name = command_data
            command_params = {}
        command_name = command_name.replace("-", "_")
        # Prevent private methods from being accessed as commands
        command_name = command_name.strip("_")
        return command_name, command_params

    def _map_command(self, command_data):
        """Map a directive from the `installer` section to an internal
        method."""
        command_name, command_params = self._get_command_name_and_params(command_data)
        if not hasattr(self, command_name):
            raise ScriptingError(_('The command "%s" does not exist.') % command_name)
        return getattr(self, command_name), command_params

    def _finish_install(self):
        game_id = self.installer.save()

        launcher_value = None
        if self.installer.script.get("game"):
            _launcher, launcher_value = get_game_launcher(self.installer.script)
        path = None
        if launcher_value:
            path = self._substitute(launcher_value)
            if not os.path.isabs(path) and self.target_path:
                path = os.path.join(self.target_path, path)
        if path and not os.path.isfile(path) and self.installer.runner not in ("web", "browser"):
            self.parent.set_status(
                _(
                    "The executable at path %s can't be found, please check the destination folder.\n"
                    "Some parts of the installation process may have not completed successfully."
                ) % path
            )
            logger.warning("No executable found at specified location %s", path)
        else:
            install_complete_text = (self.installer.script.get("install_complete_text") or _("Installation completed!"))
            self.parent.set_status(install_complete_text)
        download_lutris_media(self.installer.game_slug)
        self.parent.on_install_finished(game_id)

    def cleanup(self):
        """Clean up install dir after a successful install"""
        os.chdir(os.path.expanduser("~"))
        system.remove_folder(self.cache_path)

    def revert(self, remove_game_dir=True):
        """Revert installation in case of an error"""
        logger.info("Cancelling installation of %s", self.installer.game_name)
        if self.installer.runner.startswith("wine"):
            self.task({"name": "winekill"})

        self.cancelled = True

        if self.abort_current_task:
            self.abort_current_task()

        if self.target_path and remove_game_dir:
            system.remove_folder(self.target_path)

    def _get_string_replacements(self):
        """Return a mapping of variables to their actual value"""
        replacements = {
            "GAMEDIR": self.target_path,
            "CACHE": self.cache_path,
            "HOME": os.path.expanduser("~"),
            "STEAM_DATA_DIR": steam.steam().steam_data_dir,
            "DISC": self.game_disc,
            "USER": os.getenv("USER"),
            "INPUT": self.user_inputs[-1]["value"] if self.user_inputs else "",
            "VERSION": self.installer.version,
            "RESOLUTION": "x".join(self.current_resolution),
            "RESOLUTION_WIDTH": self.current_resolution[0],
            "RESOLUTION_HEIGHT": self.current_resolution[1],
            "RESOLUTION_WIDTH_HEX": hex(int(self.current_resolution[0])),
            "RESOLUTION_HEIGHT_HEX": hex(int(self.current_resolution[1])),
            "WINEBIN": self.get_wine_path(),
        }
        replacements.update(self.installer.variables)
        # Add 'INPUT_<id>' replacements for user inputs with an id
        for input_data in self.user_inputs:
            alias = input_data["alias"]
            if alias:
                replacements[alias] = input_data["value"]
        replacements.update(self.game_files)
        return replacements

    def _substitute(self, template_string):
        """Replace path aliases with real paths."""
        if template_string is None:
            logger.warning("No template string given")
            return ""
        if str(template_string).replace("-", "_") in self.game_files:
            template_string = template_string.replace("-", "_")
        return system.substitute(template_string, self._get_string_replacements())

    def eject_wine_disc(self):
        """Use Wine to eject a CD, otherwise Wine can have problems detecting disc changes"""
        wine_path = get_wine_version_exe(self._get_runner_version())
        wine.eject_disc(wine_path, self.target_path)
appid property readonly
cache_path property readonly

Return the directory used as a cache for the duration of the installation

script_env property readonly

Return the script's own environment variable with values susbtituted. This value can be used to provide the same environment variable as set for the game during the install process.

__init__(self, installer, parent=None) special
Source code in lutris/installer/interpreter.py
def __init__(self, installer, parent=None):
    super().__init__()
    self.target_path = None
    self.parent = parent
    self.service = parent.service if parent else None
    _appid = parent.appid if parent else None
    self.game_dir_created = False  # Whether a game folder was created during the install
    # Extra files for installers, either None if the extras haven't been checked yet.
    # Or a list of IDs of extras to be downloaded during the install
    self.extras = None
    self.game_disc = None
    self.game_files = {}
    self.cancelled = False
    self.abort_current_task = None
    self.user_inputs = []
    self.current_command = 0  # Current installer command when iterating through them
    self.runners_to_install = []
    self.installer = LutrisInstaller(installer, self, service=self.service, appid=_appid)
    if not self.installer.script:
        raise ScriptingError(_("This installer doesn't have a 'script' section"))
    script_errors = self.installer.get_errors()
    if script_errors:
        raise ScriptingError(
            _("Invalid script: \n{}").format("\n".join(script_errors)), self.installer.script
        )

    self.current_resolution = DISPLAY_MANAGER.get_current_resolution()
    self._check_binary_dependencies()
    self._check_dependency()
    if self.installer.creates_game_folder:
        self.target_path = self.get_default_target()
cleanup(self)

Clean up install dir after a successful install

Source code in lutris/installer/interpreter.py
def cleanup(self):
    """Clean up install dir after a successful install"""
    os.chdir(os.path.expanduser("~"))
    system.remove_folder(self.cache_path)
create_game_folder(self)

Create the game folder if needed and store if is was created

Source code in lutris/installer/interpreter.py
def create_game_folder(self):
    """Create the game folder if needed and store if is was created"""
    if (
            self.installer.files
            and self.target_path
            and not system.path_exists(self.target_path)
            and self.installer.creates_game_folder
    ):
        try:
            logger.debug("Creating destination path %s", self.target_path)
            os.makedirs(self.target_path)
            self.game_dir_created = True
        except PermissionError as err:
            raise ScriptingError(
                _("Lutris does not have the necessary permissions to install to path:"),
                self.target_path,
            ) from err
eject_wine_disc(self)

Use Wine to eject a CD, otherwise Wine can have problems detecting disc changes

Source code in lutris/installer/interpreter.py
def eject_wine_disc(self):
    """Use Wine to eject a CD, otherwise Wine can have problems detecting disc changes"""
    wine_path = get_wine_version_exe(self._get_runner_version())
    wine.eject_disc(wine_path, self.target_path)
get_default_target(self)

Return default installation dir

Source code in lutris/installer/interpreter.py
def get_default_target(self):
    """Return default installation dir"""
    config = LutrisConfig(runner_slug=self.installer.runner)
    games_dir = config.system_config.get("game_path", os.path.expanduser("~"))
    if self.service:
        service_dir = self.service.id
    else:
        service_dir = ""
    return os.path.expanduser(os.path.join(games_dir, service_dir, self.installer.game_slug))
get_extras(self)

Get extras and store them to move them at the end of the install

Source code in lutris/installer/interpreter.py
def get_extras(self):
    """Get extras and store them to move them at the end of the install"""
    logger.debug("Checking if service provide extra files")
    if not self.service or not self.service.has_extras:
        self.extras = []
        return self.extras
    self.extras = self.service.get_extras(self.installer.service_appid)
    return self.extras
get_runner_class(self, runner_name)

Runner the runner class from its name

Source code in lutris/installer/interpreter.py
def get_runner_class(self, runner_name):
    """Runner the runner class from its name"""
    try:
        runner = import_runner(runner_name)
    except InvalidRunner as err:
        GLib.idle_add(self.parent.cancel_button.set_sensitive, True)
        raise ScriptingError(_("Invalid runner provided %s") % runner_name) from err
    return runner
get_runners_to_install(self)

Check if the runner is installed before starting the installation Install the required runner(s) if necessary. This should handle runner dependencies or runners used for installer tasks.

Source code in lutris/installer/interpreter.py
def get_runners_to_install(self):
    """Check if the runner is installed before starting the installation
    Install the required runner(s) if necessary. This should handle runner
    dependencies or runners used for installer tasks.
    """
    runners_to_install = []
    required_runners = []
    runner = self.get_runner_class(self.installer.runner)
    required_runners.append(runner())

    for command in self.installer.script.get("installer", []):
        command_name, command_params = self._get_command_name_and_params(command)
        if command_name == "task":
            runner_name, _task_name = self._get_task_runner_and_name(command_params["name"])
            runner_names = [r.name for r in required_runners]
            if runner_name not in runner_names:
                required_runners.append(self.get_runner_class(runner_name)())

    for runner in required_runners:
        params = {}
        if self.installer.runner == "libretro":
            params["core"] = self.installer.script["game"]["core"]
        if self.installer.runner.startswith("wine"):
            # Force the wine version to be installed
            params["fallback"] = False
            params["min_version"] = wine.MIN_SAFE_VERSION
            version = self._get_runner_version()
            if version:
                params["version"] = version
            else:
                # Looking up default wine version
                default_wine = runner.get_runner_version() or {}
                if "version" in default_wine:
                    logger.debug("Default wine version is %s", default_wine["version"])
                    # Set the version to both the is_installed params and
                    # the script itself so the version gets saved at the
                    # end of the install.
                    if self.installer.runner not in self.installer.script:
                        self.installer.script[self.installer.runner] = {}
                    version = "{}-{}".format(default_wine["version"],
                                             default_wine["architecture"])
                    params["version"] = \
                        self.installer.script[self.installer.runner]["version"] = version
                else:
                    logger.error("Failed to get default wine version (got %s)", default_wine)

        if not runner.is_installed(**params):
            logger.info("Runner %s needs to be installed", runner)
            runners_to_install.append(runner)

    if self.installer.runner.startswith("wine") and not get_wine_version():
        WineNotInstalledWarning(parent=self.parent)
    return runners_to_install
install_runner(self, runner)

Install runner required by the install script

Source code in lutris/installer/interpreter.py
def install_runner(self, runner):
    """Install runner required by the install script"""
    logger.debug("Installing %s", runner.name)
    try:
        runner.install(
            version=self._get_runner_version(),
            downloader=simple_downloader,
            callback=self.install_runners,
        )
    except (NonInstallableRunnerError, RunnerInstallationError) as ex:
        logger.error(ex.message)
        raise ScriptingError(ex.message) from ex
install_runners(self)

Install required runners for a game

Source code in lutris/installer/interpreter.py
def install_runners(self):
    """Install required runners for a game"""
    if self.runners_to_install:
        self.install_runner(self.runners_to_install.pop(0))
        return
    self.emit("runners-installed")
launch_install(self)

Launch the install process

Source code in lutris/installer/interpreter.py
def launch_install(self):
    """Launch the install process"""
    self.runners_to_install = self.get_runners_to_install()
    self.install_runners()
    self.create_game_folder()
launch_installer_commands(self)

Run the pre-installation steps and launch install.

Source code in lutris/installer/interpreter.py
def launch_installer_commands(self):
    """Run the pre-installation steps and launch install."""
    if self.target_path and os.path.exists(self.target_path):
        os.chdir(self.target_path)
    os.makedirs(self.cache_path, exist_ok=True)

    # Copy extras to game folder
    if len(self.extras) == len(self.installer.files):
        # Reset the install script in case there are only extras.
        logger.warning("Installer with only extras and no game files")
        self.installer.script["installer"] = []

    for extra in self.extras:
        self.installer.script["installer"].append(
            {"copy": {"src": extra, "dst": "$GAMEDIR/extras"}}
        )
    self._iter_commands()
revert(self, remove_game_dir=True)

Revert installation in case of an error

Source code in lutris/installer/interpreter.py
def revert(self, remove_game_dir=True):
    """Revert installation in case of an error"""
    logger.info("Cancelling installation of %s", self.installer.game_name)
    if self.installer.runner.startswith("wine"):
        self.task({"name": "winekill"})

    self.cancelled = True

    if self.abort_current_task:
        self.abort_current_task()

    if self.target_path and remove_game_dir:
        system.remove_folder(self.target_path)

legacy

get_game_launcher(script)

Return the key and value of the launcher exe64 can be provided to specify an executable for 64bit systems This should be deprecated when support for multiple binaries has been added.

Source code in lutris/installer/legacy.py
def get_game_launcher(script):
    """Return the key and value of the launcher
    exe64 can be provided to specify an executable for 64bit systems
    This should be deprecated when support for multiple binaries has been
    added.
    """
    launcher_value = None
    exe = "exe64" if "exe64" in script and linux.LINUX_SYSTEM.is_64_bit else "exe"
    for launcher in (exe, "iso", "rom", "disk", "main_file"):
        if launcher not in script:
            continue
        launcher_value = script[launcher]
        if launcher == "exe64":
            launcher = "exe"  # If exe64 is used, rename it to exe
        break
    if not launcher_value:
        launcher = None
    return launcher, launcher_value

steam_installer

Collection of installer files

SteamInstaller (Object)

Handles installation of Steam games

Source code in lutris/installer/steam_installer.py
class SteamInstaller(GObject.Object):
    """Handles installation of Steam games"""

    __gsignals__ = {
        "steam-game-installed": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
        "steam-state-changed": (GObject.SIGNAL_RUN_FIRST, None, (str, )),
    }

    def __init__(self, steam_uri, file_id):
        """
        Params:
            steam_uri: Colon separated game info containing:
                    - $STEAM
                    - The Steam appid
                    - The relative path of files to retrieve
            file_id: The lutris installer internal id for the game files
        """
        super().__init__()
        self.steam_poll = None
        self.prev_states = []  # Previous states for the Steam installer
        self.state = ""
        self.install_start_time = None
        self.steam_uri = steam_uri
        self.stop_func = None
        self.cancelled = False
        self._runner = None

        self.file_id = file_id
        try:
            _steam, appid, path = self.steam_uri.split(":", 2)
        except ValueError as err:
            raise ScriptingError(_("Malformed steam path: %s") % self.steam_uri) from err

        self.appid = appid
        self.path = path
        self.platform = "linux"
        self.runner = steam.steam()

    @property
    def steam_rel_path(self):
        """Return the relative path for data files"""
        _steam_rel_path = self.path.strip()
        if _steam_rel_path == "/":
            _steam_rel_path = "."
        return _steam_rel_path

    @staticmethod
    def on_steam_game_installed(_data, error):
        """Callback for Steam game installer, mostly for error handling
        since install progress is handled by _monitor_steam_game_install
        """
        if error:
            raise ScriptingError(str(error))

    def install_steam_game(self):
        """Launch installation of a steam game"""
        if self.runner.get_game_path_from_appid(appid=self.appid):
            logger.info("Steam game %s is already installed", self.appid)
            self.emit("steam-game-installed", self.appid)
        else:
            logger.debug("Installing steam game %s", self.appid)
            self.runner.config = LutrisConfig(runner_slug=self.runner.name)
            AsyncCall(self.runner.install_game, self.on_steam_game_installed, self.appid)
            self.install_start_time = time.localtime()
            self.steam_poll = GLib.timeout_add(2000, self._monitor_steam_game_install)
            self.stop_func = lambda: self.runner.remove_game_data(appid=self.appid)

    def get_steam_data_path(self):
        """Return path of Steam files"""
        data_path = self.runner.get_game_path_from_appid(appid=self.appid)
        if not data_path or not os.path.exists(data_path):
            logger.info("No path found for Steam game %s", self.appid)
            return ""
        return os.path.abspath(
            os.path.join(data_path, self.steam_rel_path)
        )

    def _monitor_steam_game_install(self):
        if self.cancelled:
            return False
        states = get_app_state_log(
            self.runner.steam_data_dir, self.appid, self.install_start_time
        )
        if states and states != self.prev_states:
            self.state = states[-1].split(",")[-1]
            logger.debug("Steam installation status: %s", states)
            self.emit("steam-state-changed", self.state)  # Broadcast new state to listeners

        self.prev_states = states
        logger.debug(self.state)
        logger.debug(states)
        if self.state == "Fully Installed":
            logger.info("Steam game %s has been installed successfully", self.appid)
            self.emit("steam-game-installed", self.appid)
            return False
        return True
steam_rel_path property readonly

Return the relative path for data files

__init__(self, steam_uri, file_id) special

Parameters:

Name Type Description Default
steam_uri

Colon separated game info containing: - $STEAM - The Steam appid - The relative path of files to retrieve

required
file_id

The lutris installer internal id for the game files

required
Source code in lutris/installer/steam_installer.py
def __init__(self, steam_uri, file_id):
    """
    Params:
        steam_uri: Colon separated game info containing:
                - $STEAM
                - The Steam appid
                - The relative path of files to retrieve
        file_id: The lutris installer internal id for the game files
    """
    super().__init__()
    self.steam_poll = None
    self.prev_states = []  # Previous states for the Steam installer
    self.state = ""
    self.install_start_time = None
    self.steam_uri = steam_uri
    self.stop_func = None
    self.cancelled = False
    self._runner = None

    self.file_id = file_id
    try:
        _steam, appid, path = self.steam_uri.split(":", 2)
    except ValueError as err:
        raise ScriptingError(_("Malformed steam path: %s") % self.steam_uri) from err

    self.appid = appid
    self.path = path
    self.platform = "linux"
    self.runner = steam.steam()
get_steam_data_path(self)

Return path of Steam files

Source code in lutris/installer/steam_installer.py
def get_steam_data_path(self):
    """Return path of Steam files"""
    data_path = self.runner.get_game_path_from_appid(appid=self.appid)
    if not data_path or not os.path.exists(data_path):
        logger.info("No path found for Steam game %s", self.appid)
        return ""
    return os.path.abspath(
        os.path.join(data_path, self.steam_rel_path)
    )
install_steam_game(self)

Launch installation of a steam game

Source code in lutris/installer/steam_installer.py
def install_steam_game(self):
    """Launch installation of a steam game"""
    if self.runner.get_game_path_from_appid(appid=self.appid):
        logger.info("Steam game %s is already installed", self.appid)
        self.emit("steam-game-installed", self.appid)
    else:
        logger.debug("Installing steam game %s", self.appid)
        self.runner.config = LutrisConfig(runner_slug=self.runner.name)
        AsyncCall(self.runner.install_game, self.on_steam_game_installed, self.appid)
        self.install_start_time = time.localtime()
        self.steam_poll = GLib.timeout_add(2000, self._monitor_steam_game_install)
        self.stop_func = lambda: self.runner.remove_game_data(appid=self.appid)
on_steam_game_installed(_data, error) staticmethod

Callback for Steam game installer, mostly for error handling since install progress is handled by _monitor_steam_game_install

Source code in lutris/installer/steam_installer.py
@staticmethod
def on_steam_game_installed(_data, error):
    """Callback for Steam game installer, mostly for error handling
    since install progress is handled by _monitor_steam_game_install
    """
    if error:
        raise ScriptingError(str(error))

migrations special

MIGRATIONS

MIGRATION_VERSION

get_migration_module(migration_name)

Source code in lutris/migrations/__init__.py
def get_migration_module(migration_name):
    return importlib.import_module("lutris.migrations.%s" % migration_name)

migrate()

Source code in lutris/migrations/__init__.py
def migrate():
    current_version = int(settings.read_setting("migration_version") or 0)
    if current_version >= MIGRATION_VERSION:
        return
    for i in range(current_version, MIGRATION_VERSION):
        for migration_name in MIGRATIONS[i]:
            logger.info("Running migration: %s", migration_name)
            migration = get_migration_module(migration_name)
            migration.migrate()

    settings.write_setting("migration_version", MIGRATION_VERSION)

mess_to_mame

Migrate MESS games to MAME

migrate()

Run migration

Source code in lutris/migrations/mess_to_mame.py
def migrate():
    """Run migration"""
    for pga_game in get_games():
        game = Game(pga_game["id"])
        if game.runner_name != "mess":
            continue
        if "mess" in game.config.game_level:
            game.config.game_level["mame"] = game.config.game_level.pop("mess")
        game.runner_name = "mame"
        game.save()

migrate_banners

Migrate banners from .local/share/lutris to .cache/lutris

migrate()

Source code in lutris/migrations/migrate_banners.py
def migrate():
    dest_dir = settings.BANNER_PATH
    src_dir = os.path.join(settings.DATA_DIR, "banners")

    try:
        # init_lutris() creates the new banners directrory
        if os.path.isdir(src_dir) and os.path.isdir(dest_dir):
            for filename in os.listdir(src_dir):
                src_file = os.path.join(src_dir, filename)
                dest_file = os.path.join(dest_dir, filename)

                if not os.path.exists(dest_file):
                    os.rename(src_file, dest_file)
                else:
                    os.unlink(src_file)

            if not os.listdir(src_dir):
                os.rmdir(src_dir)
    except OSError as ex:
        logger.exception("Failed to migrate banners: %s", ex)

migrate_hidden_ids

Move hidden games from settings to database

get_hidden_ids()

Return a list of game IDs to be excluded from the library view

Source code in lutris/migrations/migrate_hidden_ids.py
def get_hidden_ids():
    """Return a list of game IDs to be excluded from the library view"""
    # Load the ignore string and filter out empty strings to prevent issues
    ignores_raw = settings.read_setting("library_ignores", section="lutris", default="").split(",")
    ignores = [ignore for ignore in ignores_raw if not ignore == ""]

    # Turn the strings into integers
    return [int(game_id) for game_id in ignores]

migrate()

Run migration

Source code in lutris/migrations/migrate_hidden_ids.py
def migrate():
    """Run migration"""
    try:
        game_ids = get_hidden_ids()
    except:
        print("Failed to read hidden game IDs")
        return []
    for game_id in game_ids:
        game = Game(game_id)
        game.set_hidden(True)
    settings.write_setting("library_ignores", '', section="lutris")

migrate_steam_appids

Set service ID for Steam games

migrate()

Run migration

Source code in lutris/migrations/migrate_steam_appids.py
def migrate():
    """Run migration"""
    for game in get_games():
        if not game.get("steamid"):
            continue
        if game["runner"] and game["runner"] != "steam":
            continue
        print("Migrating Steam game %s" % game["name"])
        sql.db_update(
            settings.PGA_DB,
            "games",
            {"service": "steam", "service_id": game["steamid"]},
            {"id": game["id"]}
        )

runner_interpreter

Transform runner parameters to data usable for runtime execution

export_bash_script(runner, gameplay_info, script_path)

Convert runner configuration into a bash script

Source code in lutris/runner_interpreter.py
def export_bash_script(runner, gameplay_info, script_path):
    """Convert runner configuration into a bash script"""
    if getattr(runner, 'prelaunch', None) is not None:
        runner.prelaunch()
    command, env = get_launch_parameters(runner, gameplay_info)
    # Override TERM otherwise the script might not run
    env["TERM"] = "xterm"
    script_content = "#!/bin/bash\n\n\n"
    script_content += "# Environment variables\n"
    for name, value in env.items():
        script_content += 'export %s="%s"\n' % (name, value)
    script_content += "\n# Command\n"
    script_content += " ".join([shlex.quote(c) for c in command])
    with open(script_path, "w", encoding='utf-8') as script_file:
        script_file.write(script_content)

    os.chmod(script_path, os.stat(script_path).st_mode | stat.S_IEXEC)

get_gamescope_args(launch_arguments, system_config)

Insert gamescope at the start of the launch arguments

Source code in lutris/runner_interpreter.py
def get_gamescope_args(launch_arguments, system_config):
    """Insert gamescope at the start of the launch arguments"""
    launch_arguments.insert(0, "--")
    launch_arguments.insert(0, "-f")
    if system_config.get("gamescope_output_res"):
        output_width, output_height = system_config["gamescope_output_res"].lower().split("x")
        launch_arguments.insert(0, output_height)
        launch_arguments.insert(0, "-H")
        launch_arguments.insert(0, output_width)
        launch_arguments.insert(0, "-W")
    if system_config.get("gamescope_game_res"):
        game_width, game_height = system_config["gamescope_game_res"].lower().split("x")
        launch_arguments.insert(0, game_height)
        launch_arguments.insert(0, "-h")
        launch_arguments.insert(0, game_width)
        launch_arguments.insert(0, "-w")
    launch_arguments.insert(0, "gamescope")
    return launch_arguments

get_launch_parameters(runner, gameplay_info)

Source code in lutris/runner_interpreter.py
def get_launch_parameters(runner, gameplay_info):
    system_config = runner.system_config
    launch_arguments = gameplay_info["command"]
    env = {
        "DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1": "1"
    }

    # Steam compatibility
    if os.environ.get("SteamAppId"):
        logger.info("Game launched from steam (AppId: %s)", os.environ["SteamAppId"])
        env["LC_ALL"] = ""

    # Optimus
    optimus = system_config.get("optimus")
    if optimus == "primusrun" and system.find_executable("primusrun"):
        launch_arguments.insert(0, "primusrun")
    elif optimus == "optirun" and system.find_executable("optirun"):
        launch_arguments.insert(0, "virtualgl")
        launch_arguments.insert(0, "-b")
        launch_arguments.insert(0, "optirun")
    elif optimus == "pvkrun" and system.find_executable("pvkrun"):
        launch_arguments.insert(0, "pvkrun")

    mango_args, mango_env = get_mangohud_conf(system_config)
    if mango_args:
        launch_arguments = mango_args + launch_arguments
        env.update(mango_env)

    # Libstrangle
    fps_limit = system_config.get("fps_limit") or ""
    if fps_limit:
        strangle_cmd = system.find_executable("strangle")
        if strangle_cmd:
            launch_arguments = [strangle_cmd, fps_limit] + launch_arguments
        else:
            logger.warning("libstrangle is not available on this system, FPS limiter disabled")

    prefix_command = system_config.get("prefix_command") or ""
    if prefix_command:
        launch_arguments = (shlex.split(os.path.expandvars(prefix_command)) + launch_arguments)

    single_cpu = system_config.get("single_cpu") or False
    if single_cpu:
        limit_cpu_count = system_config.get("limit_cpu_count")
        if limit_cpu_count and limit_cpu_count.isnumeric():
            limit_cpu_count = int(limit_cpu_count)
        else:
            limit_cpu_count = 1

        limit_cpu_count = max(1, limit_cpu_count)
        logger.info("The game will run on %d CPU core(s)", limit_cpu_count)
        launch_arguments.insert(0, "0-%d" % (limit_cpu_count - 1))
        launch_arguments.insert(0, "-c")
        launch_arguments.insert(0, "taskset")

    env.update(runner.get_env())

    env.update(gameplay_info.get("env") or {})

    # Set environment variables dependent on gameplay info

    # LD_PRELOAD
    ld_preload = gameplay_info.get("ld_preload")
    if ld_preload:
        env["LD_PRELOAD"] = ld_preload

    # LD_LIBRARY_PATH
    game_ld_library_path = gameplay_info.get("ld_library_path")
    if game_ld_library_path:
        ld_library_path = env.get("LD_LIBRARY_PATH")
        env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
            game_ld_library_path, ld_library_path]))

    # Feral gamemode
    gamemode = system_config.get("gamemode") and LINUX_SYSTEM.gamemode_available()
    if gamemode:
        launch_arguments.insert(0, "gamemoderun")

    # Gamescope
    gamescope = system_config.get("gamescope") and system.find_executable("gamescope")
    if gamescope:
        launch_arguments = get_gamescope_args(launch_arguments, system_config)

    return launch_arguments, env

get_mangohud_conf(system_config)

Return correct launch arguments and environment variables for Mangohud.

Source code in lutris/runner_interpreter.py
def get_mangohud_conf(system_config):
    """Return correct launch arguments and environment variables for Mangohud."""
    env = {"MANGOHUD": "1"}
    mango_args = []
    mangohud = system_config.get("mangohud") or ""
    if mangohud and system.find_executable("mangohud"):
        if mangohud == "gl64":
            mango_args = ["mangohud"]
            env["MANGOHUD_DLSYM"] = "1"
        elif mangohud == "gl32":
            mango_args = ["mangohud.x86"]
            env["MANGOHUD_DLSYM"] = "1"
        else:
            mango_args = ["mangohud"]
    return mango_args, env

runners special

Runner loaders

ADDON_RUNNERS

RUNNER_NAMES

RUNNER_PLATFORMS

__all__ special

InvalidRunner (Exception)

Source code in lutris/runners/__init__.py
class InvalidRunner(Exception):

    def __init__(self, message):
        super().__init__(message)
        self.message = message

__init__(self, message) special

Source code in lutris/runners/__init__.py
def __init__(self, message):
    super().__init__(message)
    self.message = message

NonInstallableRunnerError (Exception)

Source code in lutris/runners/__init__.py
class NonInstallableRunnerError(Exception):

    def __init__(self, message):
        super().__init__(message)
        self.message = message

__init__(self, message) special

Source code in lutris/runners/__init__.py
def __init__(self, message):
    super().__init__(message)
    self.message = message

RunnerInstallationError (Exception)

Source code in lutris/runners/__init__.py
class RunnerInstallationError(Exception):

    def __init__(self, message):
        super().__init__(message)
        self.message = message

__init__(self, message) special

Source code in lutris/runners/__init__.py
def __init__(self, message):
    super().__init__(message)
    self.message = message

get_installed(sort=True)

Return a list of installed runners (class instances).

Source code in lutris/runners/__init__.py
def get_installed(sort=True):
    """Return a list of installed runners (class instances)."""
    installed = []
    for runner_name in __all__:
        runner = import_runner(runner_name)()
        if runner.is_installed():
            installed.append(runner)
    return sorted(installed) if sort else installed

get_platforms()

Return a dictionary of all supported platforms with their runners

Source code in lutris/runners/__init__.py
def get_platforms():
    """Return a dictionary of all supported platforms with their runners"""
    platforms = defaultdict(list)
    for runner_name in __all__:
        runner = import_runner(runner_name)()
        for platform in runner.platforms:
            platforms[platform].append(runner_name)
    return platforms

get_runner_module(runner_name)

Source code in lutris/runners/__init__.py
def get_runner_module(runner_name):
    if runner_name not in __all__:
        raise InvalidRunner("Invalid runner name '%s'" % runner_name)
    return __import__("lutris.runners.%s" % runner_name, globals(), locals(), [runner_name], 0)

get_runner_names()

Source code in lutris/runners/__init__.py
def get_runner_names():
    return {
        runner: import_runner(runner)().human_name for runner in __all__
    }

import_runner(runner_name)

Dynamically import a runner class.

Source code in lutris/runners/__init__.py
def import_runner(runner_name):
    """Dynamically import a runner class."""
    if runner_name in ADDON_RUNNERS:
        return ADDON_RUNNERS[runner_name]

    runner_module = get_runner_module(runner_name)
    if not runner_module:
        return None
    return getattr(runner_module, runner_name)

import_task(runner, task)

Return a runner task.

Source code in lutris/runners/__init__.py
def import_task(runner, task):
    """Return a runner task."""
    runner_module = get_runner_module(runner)
    if not runner_module:
        return None
    return getattr(runner_module, task)

inject_runners(runners)

Source code in lutris/runners/__init__.py
def inject_runners(runners):
    for runner_name in runners:
        ADDON_RUNNERS[runner_name] = runners[runner_name]
        __all__.append(runner_name)

atari800

atari800 (Runner)

Source code in lutris/runners/atari800.py
class atari800(Runner):
    human_name = _("Atari800")
    platforms = [_("Atari 8bit computers")]  # FIXME try to determine the actual computer used
    runner_executable = "atari800/bin/atari800"
    bios_url = "http://kent.dl.sourceforge.net/project/atari800/ROM/Original%20XL%20ROM/xf25.zip"
    description = _("Atari 400, 800 and XL emulator")
    bios_checksums = {
        "xlxe_rom": "06daac977823773a3eea3422fd26a703",
        "basic_rom": "0bac0c6a50104045d902df4503a4c30b",
        "osa_rom": "",
        "osb_rom": "a3e8d617c95d08031fe1b20d541434b2",
        "5200_rom": "",
    }
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "help": _(
                "The game data, commonly called a ROM image. \n"
                "Supported formats: ATR, XFD, DCM, ATR.GZ, XFD.GZ "
                "and PRO."
            ),
        }
    ]

    runner_options = [
        {
            "option":
            "bios_path",
            "type":
            "directory_chooser",
            "label":
            _("BIOS location"),
            "help": _(
                "A folder containing the Atari 800 BIOS files.\n"
                "They are provided by Lutris so you shouldn't have to "
                "change this."
            ),
        },
        {
            "option":
            "machine",
            "type":
            "choice",
            "choices": [
                (_("Emulate Atari 800"), "atari"),
                (_("Emulate Atari 800 XL"), "xl"),
                (_("Emulate Atari 320 XE (Compy Shop)"), "320xe"),
                (_("Emulate Atari 320 XE (Rambo)"), "rambo"),
                (_("Emulate Atari 5200"), "5200"),
            ],
            "default":
            "atari",
            "label":
            _("Machine"),
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "default": False,
            "label": _("Fullscreen"),
        },
        {
            "option": "resolution",
            "type": "choice",
            "choices": get_resolutions(),
            "default": "desktop",
            "label": _("Fullscreen resolution"),
        },
    ]

    def install(self, version=None, downloader=None, callback=None):

        def on_runner_installed(*args):  # pylint: disable=unused-argument
            config_path = system.create_folder("~/.atari800")
            bios_archive = os.path.join(config_path, "atari800-bioses.zip")
            dlg = DownloadDialog(self.bios_url, bios_archive)
            dlg.run()
            if not system.path_exists(bios_archive):
                ErrorDialog(_("Could not download Atari 800 BIOS archive"))
                return
            extract.extract_archive(bios_archive, config_path)
            os.remove(bios_archive)
            config = LutrisConfig(runner_slug="atari800")
            config.raw_runner_config.update({"bios_path": config_path})
            config.save()
            if callback:
                callback()

        super().install(version, downloader, on_runner_installed)

    def find_good_bioses(self, bios_path):
        """ Check for correct bios files """
        good_bios = {}
        for filename in os.listdir(bios_path):
            real_hash = system.get_md5_hash(os.path.join(bios_path, filename))
            for bios_file, checksum in self.bios_checksums.items():
                if real_hash == checksum:
                    logging.debug("%s Checksum : OK", filename)
                    good_bios[bios_file] = filename
        return good_bios

    def play(self):
        arguments = [self.get_executable()]
        if self.runner_config.get("fullscreen"):
            arguments.append("-fullscreen")
        else:
            arguments.append("-windowed")

        resolution = self.runner_config.get("resolution")
        if resolution:
            if resolution == "desktop":
                width, height = display.DISPLAY_MANAGER.get_current_resolution()
            else:
                width, height = resolution.split("x")
            arguments += ["-fs-width", "%s" % width, "-fs-height", "%s" % height]

        if self.runner_config.get("machine"):
            arguments.append("-%s" % self.runner_config["machine"])

        bios_path = self.runner_config.get("bios_path")
        if not system.path_exists(bios_path):
            return {"error": "NO_BIOS"}
        good_bios = self.find_good_bioses(bios_path)
        for bios, filename in good_bios.items():
            arguments.append("-%s" % bios)
            arguments.append(os.path.join(bios_path, filename))

        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        arguments.append(rom)

        return {"command": arguments}
bios_checksums
bios_url
description
game_options
human_name
platforms
runner_executable
runner_options
find_good_bioses(self, bios_path)

Check for correct bios files

Source code in lutris/runners/atari800.py
def find_good_bioses(self, bios_path):
    """ Check for correct bios files """
    good_bios = {}
    for filename in os.listdir(bios_path):
        real_hash = system.get_md5_hash(os.path.join(bios_path, filename))
        for bios_file, checksum in self.bios_checksums.items():
            if real_hash == checksum:
                logging.debug("%s Checksum : OK", filename)
                good_bios[bios_file] = filename
    return good_bios
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/atari800.py
def install(self, version=None, downloader=None, callback=None):

    def on_runner_installed(*args):  # pylint: disable=unused-argument
        config_path = system.create_folder("~/.atari800")
        bios_archive = os.path.join(config_path, "atari800-bioses.zip")
        dlg = DownloadDialog(self.bios_url, bios_archive)
        dlg.run()
        if not system.path_exists(bios_archive):
            ErrorDialog(_("Could not download Atari 800 BIOS archive"))
            return
        extract.extract_archive(bios_archive, config_path)
        os.remove(bios_archive)
        config = LutrisConfig(runner_slug="atari800")
        config.raw_runner_config.update({"bios_path": config_path})
        config.save()
        if callback:
            callback()

    super().install(version, downloader, on_runner_installed)
play(self)
Source code in lutris/runners/atari800.py
def play(self):
    arguments = [self.get_executable()]
    if self.runner_config.get("fullscreen"):
        arguments.append("-fullscreen")
    else:
        arguments.append("-windowed")

    resolution = self.runner_config.get("resolution")
    if resolution:
        if resolution == "desktop":
            width, height = display.DISPLAY_MANAGER.get_current_resolution()
        else:
            width, height = resolution.split("x")
        arguments += ["-fs-width", "%s" % width, "-fs-height", "%s" % height]

    if self.runner_config.get("machine"):
        arguments.append("-%s" % self.runner_config["machine"])

    bios_path = self.runner_config.get("bios_path")
    if not system.path_exists(bios_path):
        return {"error": "NO_BIOS"}
    good_bios = self.find_good_bioses(bios_path)
    for bios, filename in good_bios.items():
        arguments.append("-%s" % bios)
        arguments.append(os.path.join(bios_path, filename))

    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    arguments.append(rom)

    return {"command": arguments}

get_resolutions()

Source code in lutris/runners/atari800.py
def get_resolutions():
    try:
        screen_resolutions = [(resolution, resolution) for resolution in display.DISPLAY_MANAGER.get_resolutions()]
    except OSError:
        screen_resolutions = []
    screen_resolutions.insert(0, (_("Desktop resolution"), "desktop"))
    return screen_resolutions

commands special

dosbox

DOSBox installer commands

dosexec(config_file=None, executable=None, args=None, close_on_exit=True, working_dir=None)

Execute Dosbox with given config_file.

Source code in lutris/runners/commands/dosbox.py
def dosexec(config_file=None, executable=None, args=None, close_on_exit=True, working_dir=None):
    """Execute Dosbox with given config_file."""
    if config_file:
        run_with = "config {}".format(config_file)
        if not working_dir:
            working_dir = os.path.dirname(config_file)
    elif executable:
        run_with = "executable {}".format(executable)
        if not working_dir:
            working_dir = os.path.dirname(executable)
    else:
        raise ValueError("Neither a config file or an executable were provided")
    logger.debug("Running dosbox with %s", run_with)
    working_dir = system.create_folder(working_dir)
    dosbox = import_runner("dosbox")
    dosbox_runner = dosbox()
    command = [dosbox_runner.get_executable()]
    if config_file:
        command += ["-conf", config_file]
    if executable:
        if not system.path_exists(executable):
            raise OSError("Can't find file {}".format(executable))
        command += [executable]
    if args:
        command += args.split()
    if close_on_exit:
        command.append("-exit")
    system.execute(command, cwd=working_dir, env=runtime.get_env())
makeconfig(path, drives, commands)
Source code in lutris/runners/commands/dosbox.py
def makeconfig(path, drives, commands):
    system.create_folder(os.path.dirname(path))
    with open(path, "w", encoding='utf-8') as config_file:
        config_file.write("[autoexec]\n")
        for drive in drives:
            config_file.write('mount {} "{}"\n'.format(drive, drives[drive]))
        for command in commands:
            config_file.write("{}\n".format(command))

wine

Wine commands for installers

create_prefix(prefix, wine_path=None, arch='win64', overrides=None, install_gecko=None, install_mono=None)

Create a new Wine prefix.

Source code in lutris/runners/commands/wine.py
def create_prefix(  # noqa: C901
    prefix,
    wine_path=None,
    arch=WINE_DEFAULT_ARCH,
    overrides=None,
    install_gecko=None,
    install_mono=None,
):
    """Create a new Wine prefix."""
    # pylint: disable=too-many-locals
    if overrides is None:
        overrides = {}
    if not prefix:
        raise ValueError("No Wine prefix path given")
    logger.info("Creating a %s prefix in %s", arch, prefix)

    # Follow symlinks, don't delete existing ones as it would break some setups
    if os.path.islink(prefix):
        prefix = os.readlink(prefix)

    # Avoid issue of 64bit Wine refusing to create win32 prefix
    # over an existing empty folder.
    if os.path.isdir(prefix) and not os.listdir(prefix):
        os.rmdir(prefix)

    if not wine_path:
        wine = import_runner("wine")
        wine_path = wine().get_executable()
    if not wine_path:
        logger.error("Wine not found, can't create prefix")
        return
    wineboot_path = os.path.join(os.path.dirname(wine_path), "wineboot")
    if not system.path_exists(wineboot_path):
        logger.error(
            "No wineboot executable found in %s, "
            "your wine installation is most likely broken",
            wine_path,
        )
        return

    wineenv = {
        "WINEARCH": arch,
        "WINEPREFIX": prefix,
        "WINEDLLOVERRIDES": get_overrides_env(overrides),
        "WINE_MONO_CACHE_DIR": os.path.join(os.path.dirname(os.path.dirname(wine_path)), "mono"),
        "WINE_GECKO_CACHE_DIR": os.path.join(os.path.dirname(os.path.dirname(wine_path)), "gecko"),
    }

    if install_gecko == "False":
        wineenv["WINE_SKIP_GECKO_INSTALLATION"] = "1"
        overrides["mshtml"] = "disabled"
    if install_mono == "False":
        wineenv["WINE_SKIP_MONO_INSTALLATION"] = "1"
        overrides["mscoree"] = "disabled"

    system.execute([wineboot_path], env=wineenv)
    for loop_index in range(1000):
        time.sleep(0.5)
        if system.path_exists(os.path.join(prefix, "user.reg")):
            break
        if loop_index == 60:
            logger.warning("Wine prefix creation is taking longer than expected...")
    if not os.path.exists(os.path.join(prefix, "user.reg")):
        logger.error("No user.reg found after prefix creation. " "Prefix might not be valid")
        return
    logger.info("%s Prefix created in %s", arch, prefix)
    prefix_manager = WinePrefixManager(prefix)
    prefix_manager.setup_defaults()
delete_registry_key(key, wine_path=None, prefix=None, arch='win64')

Deletes a registry key from a Wine prefix

Source code in lutris/runners/commands/wine.py
def delete_registry_key(key, wine_path=None, prefix=None, arch=WINE_DEFAULT_ARCH):
    """Deletes a registry key from a Wine prefix"""
    wineexec(
        "regedit",
        args='/S /D "%s"' % key,
        wine_path=wine_path,
        prefix=prefix,
        arch=arch,
        blocking=True,
    )
eject_disc(wine_path, prefix)

Use Wine to eject a drive

Source code in lutris/runners/commands/wine.py
def eject_disc(wine_path, prefix):
    """Use Wine to eject a drive"""
    wineexec("eject", prefix=prefix, wine_path=wine_path, args="-a")
install_cab_component(cabfile, component, wine_path=None, prefix=None, arch=None)

Install a component from a cabfile in a prefix

Source code in lutris/runners/commands/wine.py
def install_cab_component(cabfile, component, wine_path=None, prefix=None, arch=None):
    """Install a component from a cabfile in a prefix"""
    cab_installer = CabInstaller(prefix, wine_path=wine_path, arch=arch)
    files = cab_installer.extract_from_cab(cabfile, component)
    registry_files = cab_installer.get_registry_files(files)
    for registry_file, _arch in registry_files:
        set_regedit_file(registry_file, wine_path=wine_path, prefix=prefix, arch=_arch)
    cab_installer.cleanup()
open_wine_terminal(terminal, wine_path, prefix, env)
Source code in lutris/runners/commands/wine.py
def open_wine_terminal(terminal, wine_path, prefix, env):
    aliases = {
        "wine": wine_path,
        "winecfg": wine_path + "cfg",
        "wineserver": wine_path + "server",
        "wineboot": wine_path + "boot",
    }
    env["WINEPREFIX"] = prefix
    shell_command = get_shell_command(prefix, env, aliases)
    terminal = terminal or linux.get_default_terminal()
    system.execute([terminal, "-e", shell_command])
set_regedit(path, key, value='', type='REG_SZ', wine_path=None, prefix=None, arch='win64')

Add keys to the windows registry.

Path is something like HKEY_CURRENT_USER/Software/Wine/Direct3D

Source code in lutris/runners/commands/wine.py
def set_regedit(
    path,
    key,
    value="",
    type="REG_SZ",  # pylint: disable=redefined-builtin
    wine_path=None,
    prefix=None,
    arch=WINE_DEFAULT_ARCH,
):
    """Add keys to the windows registry.

    Path is something like HKEY_CURRENT_USER/Software/Wine/Direct3D
    """
    formatted_value = {
        "REG_SZ": '"%s"' % value,
        "REG_DWORD": "dword:" + value,
        "REG_BINARY": "hex:" + value.replace(" ", ","),
        "REG_MULTI_SZ": "hex(2):" + value,
        "REG_EXPAND_SZ": "hex(7):" + value,
    }
    # Make temporary reg file
    reg_path = os.path.join(settings.CACHE_DIR, "winekeys.reg")
    with open(reg_path, "w", encoding='utf-8') as reg_file:
        reg_file.write('REGEDIT4\n\n[%s]\n"%s"=%s\n' % (path, key, formatted_value[type]))
    logger.debug("Setting [%s]:%s=%s", path, key, formatted_value[type])
    set_regedit_file(reg_path, wine_path=wine_path, prefix=prefix, arch=arch)
    os.remove(reg_path)
set_regedit_file(filename, wine_path=None, prefix=None, arch='win64')

Apply a regedit file to the Windows registry.

Source code in lutris/runners/commands/wine.py
def set_regedit_file(filename, wine_path=None, prefix=None, arch=WINE_DEFAULT_ARCH):
    """Apply a regedit file to the Windows registry."""
    if arch == "win64" and wine_path and system.path_exists(wine_path + "64"):
        # Use wine64 by default if set to a 64bit prefix. Using regular wine
        # will prevent some registry keys from being created. Most likely to be
        # a bug in Wine. see: https://github.com/lutris/lutris/issues/804
        wine_path = wine_path + "64"

    wineexec(
        "regedit",
        args="/S '%s'" % filename,
        wine_path=wine_path,
        prefix=prefix,
        arch=arch,
        blocking=True,
    )
winecfg(wine_path=None, prefix=None, arch='win64', config=None, env=None)

Execute winecfg.

Source code in lutris/runners/commands/wine.py
def winecfg(wine_path=None, prefix=None, arch=WINE_DEFAULT_ARCH, config=None, env=None):
    """Execute winecfg."""
    if not wine_path:
        logger.debug("winecfg: Reverting to default wine")
        wine = import_runner("wine")
        wine_path = wine().get_executable()

    winecfg_path = os.path.join(os.path.dirname(wine_path), "winecfg")
    logger.debug("winecfg: %s", winecfg_path)

    return wineexec(
        None,
        prefix=prefix,
        winetricks_wine=winecfg_path,
        wine_path=winecfg_path,
        arch=arch,
        config=config,
        env=env,
        include_processes=["winecfg.exe"],
    )
wineexec(executable, args='', wine_path=None, prefix=None, arch=None, working_dir=None, winetricks_wine='', blocking=False, config=None, include_processes=None, exclude_processes=None, disable_runtime=False, env=None, overrides=None)

Execute a Wine command.

Parameters:

Name Type Description Default
executable str

wine program to run, pass None to run wine itself

required
args str

program arguments

''
wine_path str

path to the wine version to use

None
prefix str

path to the wine prefix to use

None
arch str

wine architecture of the prefix

None
working_dir str

path to the working dir for the process

None
winetricks_wine str

path to the wine version used by winetricks

''
blocking bool

if true, do not run the process in a thread

False
config LutrisConfig

LutrisConfig object for the process context

None
watch list

list of process names to monitor (even when in a ignore list)

required

Returns:

Type Description

Process results if the process is running in blocking mode or MonitoredCommand instance otherwise.

Source code in lutris/runners/commands/wine.py
def wineexec(  # noqa: C901
    executable,
    args="",
    wine_path=None,
    prefix=None,
    arch=None,
    working_dir=None,
    winetricks_wine="",
    blocking=False,
    config=None,
    include_processes=None,
    exclude_processes=None,
    disable_runtime=False,
    env=None,
    overrides=None,
):
    """
    Execute a Wine command.

    Args:
        executable (str): wine program to run, pass None to run wine itself
        args (str): program arguments
        wine_path (str): path to the wine version to use
        prefix (str): path to the wine prefix to use
        arch (str): wine architecture of the prefix
        working_dir (str): path to the working dir for the process
        winetricks_wine (str): path to the wine version used by winetricks
        blocking (bool): if true, do not run the process in a thread
        config (LutrisConfig): LutrisConfig object for the process context
        watch (list): list of process names to monitor (even when in a ignore list)

    Returns:
        Process results if the process is running in blocking mode or
        MonitoredCommand instance otherwise.
    """
    if env is None:
        env = {}
    if exclude_processes is None:
        exclude_processes = []
    if include_processes is None:
        include_processes = []
    executable = str(executable) if executable else ""
    if isinstance(include_processes, str):
        include_processes = shlex.split(include_processes)
    if isinstance(exclude_processes, str):
        exclude_processes = shlex.split(exclude_processes)

    wine = import_runner("wine")()

    if not wine_path:
        wine_path = wine.get_executable()
    if not wine_path:
        raise RuntimeError("Wine is not installed")

    if not working_dir:
        if os.path.isfile(executable):
            working_dir = os.path.dirname(executable)

    executable, _args, working_dir = get_real_executable(executable, working_dir)
    if _args:
        args = '{} "{}"'.format(_args[0], _args[1])

    # Create prefix if necessary
    if arch not in ("win32", "win64"):
        arch = detect_arch(prefix, wine_path)
    if not detect_prefix_arch(prefix):
        wine_bin = winetricks_wine if winetricks_wine else wine_path
        create_prefix(prefix, wine_path=wine_bin, arch=arch)

    wineenv = {"WINEARCH": arch}
    if winetricks_wine:
        wineenv["WINE"] = winetricks_wine
    else:
        wineenv["WINE"] = wine_path

    if prefix:
        wineenv["WINEPREFIX"] = prefix

    wine_system_config = config.system_config if config else wine.system_config
    disable_runtime = disable_runtime or wine_system_config["disable_runtime"]
    if use_lutris_runtime(wine_path=wineenv["WINE"], force_disable=disable_runtime):
        if WINE_DIR in wine_path:
            wine_root_path = os.path.dirname(os.path.dirname(wine_path))
        elif WINE_DIR in winetricks_wine:
            wine_root_path = os.path.dirname(os.path.dirname(winetricks_wine))
        else:
            wine_root_path = None
        wineenv["LD_LIBRARY_PATH"] = ":".join(
            runtime.get_paths(
                prefer_system_libs=wine_system_config["prefer_system_libs"],
                wine_path=wine_root_path,
            )
        )

    if overrides:
        wineenv["WINEDLLOVERRIDES"] = get_overrides_env(overrides)

    baseenv = wine.get_env()
    baseenv.update(wineenv)
    baseenv.update(env)

    command_parameters = [wine_path]
    if executable:
        command_parameters.append(executable)
    command_parameters += split_arguments(args)

    wine.prelaunch()

    if blocking:
        return system.execute(command_parameters, env=wineenv, cwd=working_dir)

    command = MonitoredCommand(
        command_parameters,
        runner=wine,
        env=baseenv,
        cwd=working_dir,
        include_processes=include_processes,
        exclude_processes=exclude_processes,
    )
    command.start()
    return command
winekill(prefix, arch='win64', wine_path=None, env=None, initial_pids=None)

Kill processes in Wine prefix.

Source code in lutris/runners/commands/wine.py
def winekill(prefix, arch=WINE_DEFAULT_ARCH, wine_path=None, env=None, initial_pids=None):
    """Kill processes in Wine prefix."""

    initial_pids = initial_pids or []

    if not wine_path:
        wine = import_runner("wine")
        wine_path = wine().get_executable()
    wine_root = os.path.dirname(wine_path)
    if not env:
        env = {"WINEARCH": arch, "WINEPREFIX": prefix}
    command = [os.path.join(wine_root, "wineserver"), "-k"]

    logger.debug("Killing all wine processes: %s", command)
    logger.debug("\tWine prefix: %s", prefix)
    logger.debug("\tWine arch: %s", arch)
    if initial_pids:
        logger.debug("\tInitial pids: %s", initial_pids)

    system.execute(command, env=env, quiet=True)

    logger.debug("Waiting for wine processes to terminate")
    # Wineserver needs time to terminate processes
    num_cycles = 0
    while True:
        num_cycles += 1
        running_processes = [pid for pid in initial_pids if system.path_exists("/proc/%s" % pid)]

        if not running_processes:
            break
        if num_cycles > 20:
            logger.warning(
                "Some wine processes are still running: %s",
                ", ".join(running_processes),
            )
            break
        time.sleep(0.1)
    logger.debug("Done waiting.")
winetricks(app, prefix=None, arch=None, silent=True, wine_path=None, config=None, env=None, disable_runtime=False)

Execute winetricks.

Source code in lutris/runners/commands/wine.py
def winetricks(
    app,
    prefix=None,
    arch=None,
    silent=True,
    wine_path=None,
    config=None,
    env=None,
    disable_runtime=False,
):
    """Execute winetricks."""
    wine_config = config or LutrisConfig(runner_slug="wine")
    winetricks_path = os.path.join(settings.RUNTIME_DIR, "winetricks/winetricks")
    if (wine_config.runner_config.get("system_winetricks") or not system.path_exists(winetricks_path)):
        winetricks_path = system.find_executable("winetricks")
        if not winetricks_path:
            raise RuntimeError("No installation of winetricks found")
    if wine_path:
        winetricks_wine = wine_path
    else:
        wine = import_runner("wine")
        winetricks_wine = wine().get_executable()
    if arch not in ("win32", "win64"):
        arch = detect_arch(prefix, winetricks_wine)
    args = app
    if str(silent).lower() in ("yes", "on", "true"):
        args = "--unattended " + args
    return wineexec(
        None,
        prefix=prefix,
        winetricks_wine=winetricks_wine,
        wine_path=winetricks_path,
        arch=arch,
        args=args,
        config=config,
        env=env,
        disable_runtime=disable_runtime,
    )

dolphin

Dolphin runner

dolphin (Runner)

Source code in lutris/runners/dolphin.py
class dolphin(Runner):
    description = _("GameCube and Wii emulator")
    human_name = _("Dolphin")
    platforms = [_("Nintendo GameCube"), _("Nintendo Wii")]
    require_libs = ["libOpenGL.so.0", ]
    runnable_alone = True
    runner_executable = "dolphin/dolphin-emu"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "default_path": "game_path",
            "label": _("ISO file"),
        },
        {
            "option": "platform",
            "type": "choice",
            "label": _("Platform"),
            "choices": ((_("Nintendo GameCube"), "0"), (_("Nintendo Wii"), "1")),
        },
    ]
    runner_options = [
        {
            "option": "nogui",
            "type": "bool",
            "label": _("No GUI"),
            "default": False,
            "help": _("Disable the graphical user interface."),
        },
        {
            "option": "batch",
            "type": "bool",
            "label": _("Batch"),
            "default": True,
            "advanced": True,
            "help": _("Exit Dolphin with emulator."),
        },
        {
            "option": "user_directory",
            "type": "directory_chooser",
            "advanced": True,
            "label": _("Custom Global User Directory"),
        },
    ]

    def get_platform(self):
        selected_platform = self.game_config.get("platform")
        if selected_platform:
            return self.platforms[int(selected_platform)]
        return ""

    def play(self):
        # Find the executable
        executable = self.get_executable()
        if self.runner_config.get("nogui"):
            executable += "-nogui"
        command = [executable]

        # Batch isn't available in nogui
        if self.runner_config.get("batch") and not self.runner_config.get("nogui"):
            command.append("--batch")

        # Custom Global User Directory
        if self.runner_config.get("user_directory"):
            command.append("-u")
            command.append(self.runner_config["user_directory"])

        # Retrieve the path to the file
        iso = self.game_config.get("main_file") or ""
        if not system.path_exists(iso):
            return {"error": "FILE_NOT_FOUND", "file": iso}
        command.extend(["-e", iso])

        return {"command": command}
description
game_options
human_name
platforms
require_libs
runnable_alone
runner_executable
runner_options
get_platform(self)
Source code in lutris/runners/dolphin.py
def get_platform(self):
    selected_platform = self.game_config.get("platform")
    if selected_platform:
        return self.platforms[int(selected_platform)]
    return ""
play(self)
Source code in lutris/runners/dolphin.py
def play(self):
    # Find the executable
    executable = self.get_executable()
    if self.runner_config.get("nogui"):
        executable += "-nogui"
    command = [executable]

    # Batch isn't available in nogui
    if self.runner_config.get("batch") and not self.runner_config.get("nogui"):
        command.append("--batch")

    # Custom Global User Directory
    if self.runner_config.get("user_directory"):
        command.append("-u")
        command.append(self.runner_config["user_directory"])

    # Retrieve the path to the file
    iso = self.game_config.get("main_file") or ""
    if not system.path_exists(iso):
        return {"error": "FILE_NOT_FOUND", "file": iso}
    command.extend(["-e", iso])

    return {"command": command}

dosbox

dosbox (Runner)

Source code in lutris/runners/dosbox.py
class dosbox(Runner):
    human_name = _("DOSBox")
    description = _("MS-DOS emulator")
    platforms = [_("MS-DOS")]
    runnable_alone = True
    runner_executable = "dosbox/bin/dosbox"
    require_libs = ["libopusfile.so.0", ]
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("Main file"),
            "help": _(
                "The CONF, EXE, COM or BAT file to launch.\n"
                "It can be left blank if the launch of the executable is "
                "managed in the config file."
            ),
        },
        {
            "option": "config_file",
            "type": "file",
            "label": _("Configuration file"),
            "help": _(
                "Start DOSBox with the options specified in this file. \n"
                "It can have a section in which you can put commands "
                "to execute on startup. Read DOSBox's documentation "
                "for more information."
            ),
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Command line arguments"),
            "help": _("Command line arguments used when launching DOSBox"),
            "validator": shlex.split,
        },
        {
            "option": "working_dir",
            "type": "directory_chooser",
            "label": _("Working directory"),
            "help": _(
                "The location where the game is run from.\n"
                "By default, Lutris uses the directory of the "
                "executable."
            ),
        },
    ]

    scaler_modes = [
        (_("none"), "none"),
        ("normal2x", "normal2x"),
        ("normal3x", "normal3x"),
        ("hq2x", "hq2x"),
        ("hq3x", "hq3x"),
        ("advmame2x", "advmame2x"),
        ("advmame3x", "advmame3x"),
        ("2xsai", "2xsai"),
        ("super2xsai", "super2xsai"),
        ("supereagle", "supereagle"),
        ("advinterp2x", "advinterp2x"),
        ("advinterp3x", "advinterp3x"),
        ("tv2x", "tv2x"),
        ("tv3x", "tv3x"),
        ("rgb2x", "rgb2x"),
        ("rgb3x", "rgb3x"),
        ("scan2x", "scan2x"),
        ("scan3x", "scan3x"),
    ]
    runner_options = [
        {
            "option":
            "scaler",
            "label":
            _("Graphic scaler"),
            "type":
            "choice",
            "choices":
            scaler_modes,
            "default":
            "normal3x",
            "help":
            _("The algorithm used to scale up the game's base "
              "resolution, resulting in different visual styles. "),
        },
        {
            "option": "exit",
            "label": _("Exit DOSBox with the game"),
            "type": "bool",
            "default": True,
            "help": _("Shut down DOSBox when the game is quit."),
        },
        {
            "option": "fullscreen",
            "label": _("Open game in fullscreen"),
            "type": "bool",
            "default": False,
            "help": _("Tells DOSBox to launch the game in fullscreen."),
        },
    ]

    def make_absolute(self, path):
        """Return a guaranteed absolute path"""
        if not path:
            return ""
        if os.path.isabs(path):
            return path
        if self.game_data.get("directory"):
            return os.path.join(self.game_data.get("directory"), path)
        return ""

    @property
    def main_file(self):
        return self.make_absolute(self.game_config.get("main_file"))

    @property
    def working_dir(self):
        """Return the working directory to use when running the game."""
        option = self.game_config.get("working_dir")
        if option:
            return os.path.expanduser(option)
        if self.main_file:
            return os.path.dirname(self.main_file)
        return super().working_dir

    def play(self):
        main_file = self.main_file
        if not system.path_exists(main_file):
            return {"error": "FILE_NOT_FOUND", "file": main_file}
        args = shlex.split(self.game_config.get("args") or "")

        command = [self.get_executable()]

        if main_file.endswith(".conf"):
            command.append("-conf")
            command.append(main_file)
        else:
            command.append(main_file)
        # Options
        if self.game_config.get("config_file"):
            command.append("-conf")
            command.append(self.make_absolute(self.game_config["config_file"]))

        scaler = self.runner_config.get("scaler")
        if scaler and scaler != "none":
            command.append("-scaler")
            command.append(self.runner_config["scaler"])

        if self.runner_config.get("fullscreen"):
            command.append("-fullscreen")

        if self.runner_config.get("exit"):
            command.append("-exit")

        if args:
            command.extend(args)

        return {"command": command}
description
game_options
human_name
main_file property readonly
platforms
require_libs
runnable_alone
runner_executable
runner_options
scaler_modes
working_dir property readonly

Return the working directory to use when running the game.

make_absolute(self, path)

Return a guaranteed absolute path

Source code in lutris/runners/dosbox.py
def make_absolute(self, path):
    """Return a guaranteed absolute path"""
    if not path:
        return ""
    if os.path.isabs(path):
        return path
    if self.game_data.get("directory"):
        return os.path.join(self.game_data.get("directory"), path)
    return ""
play(self)
Source code in lutris/runners/dosbox.py
def play(self):
    main_file = self.main_file
    if not system.path_exists(main_file):
        return {"error": "FILE_NOT_FOUND", "file": main_file}
    args = shlex.split(self.game_config.get("args") or "")

    command = [self.get_executable()]

    if main_file.endswith(".conf"):
        command.append("-conf")
        command.append(main_file)
    else:
        command.append(main_file)
    # Options
    if self.game_config.get("config_file"):
        command.append("-conf")
        command.append(self.make_absolute(self.game_config["config_file"]))

    scaler = self.runner_config.get("scaler")
    if scaler and scaler != "none":
        command.append("-scaler")
        command.append(self.runner_config["scaler"])

    if self.runner_config.get("fullscreen"):
        command.append("-fullscreen")

    if self.runner_config.get("exit"):
        command.append("-exit")

    if args:
        command.extend(args)

    return {"command": command}

easyrpg

easyrpg (Runner)

Source code in lutris/runners/easyrpg.py
class easyrpg(Runner):
    human_name = _("EasyRPG Player")
    description = _("Runs RPG Maker 2000/2003 games")
    platforms = [_("Linux")]
    runnable_alone = True
    entry_point_option = "project_path"
    runner_executable = "easyrpg/easyrpg-player"
    download_url = "https://easyrpg.org/downloads/player/0.7.0/easyrpg-player-0.7.0-linux.tar.gz"

    game_options = [
        {
            "option": "project_path",
            "type": "directory_chooser",
            "label": _("Game directory"),
            "help": _("Select the directory of the game. (required)")
        },
        {
            "option": "encoding",
            "type": "string",
            "label": _("Encoding"),
            "help": _(
                "Instead of auto detecting the encoding or using the "
                "one in RPG_RT.ini, the specified encoding is used. "
                "Use 'auto' for automatic detection."
            )
        },
        {
            "option": "engine",
            "type": "choice",
            "label": _("Engine"),
            "help": _("Disable auto detection of the simulated engine."),
            "choices": [
                (_("Auto"), ""),
                (_("RPG Maker 2000 engine (v1.00 - v1.10)"), "rpg2k"),
                (_("RPG Maker 2000 engine (v1.50 - v1.51)"), "rpg2kv150"),
                (_("RPG Maker 2000 (English release) engine"), "rpg2ke"),
                (_("RPG Maker 2003 engine (v1.00 - v1.04)"), "rpg2k3"),
                (_("RPG Maker 2003 engine (v1.05 - v1.09a)"), "rpg2k3v105"),
                (_("RPG Maker 2003 (English release) engine"), "rpg2k3e")
            ],
            "default": ""
        },
        {
            "option": "save_path",
            "type": "directory_chooser",
            "label": _("Save path"),
            "help": _(
                "Instead of storing save files in the game directory they "
                "are stored in the specified path. The directory must exist."
            )
        },
        {
            "option": "new_game",
            "type": "bool",
            "label": _("New game"),
            "help": _("Skip the title scene and start a new game directly."),
            "default": False
        },
        {
            "option": "load_game_id",
            "type": "range",
            "label": _("Load game ID"),
            "help": _(
                "Skip the title scene and load SaveXX.lsd. "
                "Set to '0' to disable."
            ),
            "min": 0,
            "max": 99,
            "default": 0
        },
        {
            "option": "start_map_id",
            "type": "range",
            "label": _("Start map ID"),
            "help": _(
                "Overwrite the map used for new games and use "
                "MapXXXX.lmu instead. Set to '0' to disable. "
                "\n\nIncompatible with 'Load game ID'."
            ),
            "min": 0,
            "max": 9999,
            "default": 0
        },
        {
            "option": "start_position",
            "type": "string",
            "label": _("Start position"),
            "help": _(
                "Overwrite the party start position and "
                "move the party to the specified position. "
                "Provide two numbers separated by a space. "
                "\n\nIncompatible with 'Load game ID'."
            )
        },
        {
            "option": "start_party",
            "type": "string",
            "label": _("Start party"),
            "help": _(
                "Overwrite the starting party members with "
                "the actors with the specified IDs. Provide "
                "one to four numbers separated by spaces. "
                "\n\nIncompatible with 'Load game ID'."
            )
        },
        {
            "option": "battle_test",
            "type": "string",
            "label": _("Monster party"),
            "help": _("Start a battle test with the specified monster party.")
        },
        {
            "option": "record_input",
            "type": "string",
            "label": _("Record input"),
            "help": _("Records all button input to the specified log file.")
        },
        {
            "option": "replay_input",
            "type": "file",
            "label": _("Replay input"),
            "help": _(
                "Replays button input from the specified log file, "
                "as generated by 'Record input'. If the RNG seed "
                "and the state of the save file directory is also "
                "the same as it was when the log was recorded, this "
                "should reproduce an identical run to the one recorded."
            )
        },
    ]

    runner_options = [
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "help": _("Start in fullscreen mode."),
            "default": False
        },
        {
            "option": "audio",
            "type": "bool",
            "label": _("Enable audio"),
            "help": _(
                "Switch off to disable audio "
                "(in case you prefer your own music)."
            ),
            "default": True
        },
        {
            "option": "mouse",
            "type": "bool",
            "label": _("Enable mouse"),
            "help": _(
                "Use mouse click for decision and scroll wheel for lists."
            ),
            "default": False
        },
        {
            "option": "touch",
            "type": "bool",
            "label": _("Enable touch"),
            "help": _("Use one/two finger tap for decision/cancel."),
            "default": False
        },
        {
            "option": "hide_title",
            "type": "bool",
            "label": _("Hide title"),
            "help": _(
                "Hide the title background image and center the command menu."
            ),
            "default": False
        },
        {
            "option": "vsync",
            "type": "bool",
            "label": _("Enable VSync"),
            "help": _(
                "Switch off to disable VSync and use the FPS limit. "
                "VSync may or may not be supported on all platforms."
            ),
            "default": True
        },
        {
            "option": "fps_limit",
            "type": "string",
            "label": _("FPS limit"),
            "help": _(
                "Set a custom frames per second limit. If unspecified, "
                "the default is 60 FPS. Set to '0' to disable the frame "
                "limiter. This option may not be supported on all platforms."
            )
        },
        {
            "option": "show_fps",
            "type": "choice",
            "label": _("Show FPS"),
            "help": _("Enable frames per second counter."),
            "choices": [
                (_("Disabled"), "off"),
                (_("Fullscreen & title bar"), "on"),
                (_("Fullscreen, title bar & window"), "full")
            ],
            "default": "off"
        },
        {
            "option": "seed",
            "type": "string",
            "label": _("RNG seed"),
            "help": _("Seeds the random number generator")
        },
        {
            "option": "test_play",
            "type": "bool",
            "label": _("Test play"),
            "help": _("Enable TestPlay mode."),
            "default": False
        },
        {
            "option": "rtp",
            "type": "bool",
            "label": _("Enable RTP"),
            "help": _(
                "Switch off to disable support for the Runtime Package (RTP)."
            ),
            "default": True
        },
        {
            "option": "rpg2k_rtp_path",
            "type": "directory_chooser",
            "label": _("RPG2000 RTP location"),
            "help": _(
                "Full path to a directory containing an "
                "extracted RPG Maker 2000 Run-Time-Package (RTP)."
            )
        },
        {
            "option": "rpg2k3_rtp_path",
            "type": "directory_chooser",
            "label": _("RPG2003 RTP location"),
            "help": _(
                "Full path to a directory containing an "
                "extracted RPG Maker 2003 Run-Time-Package (RTP)."
            )
        },
        {
            "option": "rpg_rtp_path",
            "type": "directory_chooser",
            "label": _("Fallback RTP location"),
            "help": _("Full path to a directory containing a combined RTP.")
        },
    ]

    @property
    def game_path(self):
        game_path = self.game_data.get("directory")
        if game_path:
            return game_path

        # Default to the directory of the entry point
        entry_point = self.game_config.get(self.entry_point_option)
        if entry_point:
            return path.expanduser(entry_point)

        return ""

    def get_env(self, os_env=False):
        env = super().get_env(os_env)

        rpg2k_rtp_path = self.runner_config.get("rpg2k_rtp_path")
        if rpg2k_rtp_path:
            env["RPG2K_RTP_PATH"] = rpg2k_rtp_path

        rpg2k3_rtp_path = self.runner_config.get("rpg2k3_rtp_path")
        if rpg2k3_rtp_path:
            env["RPG2K3_RTP_PATH"] = rpg2k3_rtp_path

        rpg_rtp_path = self.runner_config.get("rpg_rtp_path")
        if rpg_rtp_path:
            env["RPG_RTP_PATH"] = rpg_rtp_path

        return env

    def get_runner_command(self):
        cmd = [self.get_executable()]

        if self.runner_config["fullscreen"]:
            cmd.append("--fullscreen")
        else:
            cmd.append("--window")

        if not self.runner_config["audio"]:
            cmd.append("--disable-audio")

        if self.runner_config["mouse"]:
            cmd.append("--enable-mouse")

        if self.runner_config["touch"]:
            cmd.append("--enable-touch")

        if self.runner_config["hide_title"]:
            cmd.append("--hide-title")

        if not self.runner_config["vsync"]:
            cmd.append("--no-vsync")

        fps_limit = self.runner_config.get("fps_limit")
        if fps_limit:
            cmd.extend(("--fps-limit", fps_limit))

        show_fps = self.runner_config.get("show_fps")
        if show_fps != "off":
            cmd.append("--show-fps")
        if show_fps == "full":
            cmd.append("--fps-render-window")

        if self.runner_config["test_play"]:
            cmd.append("--test-play")

        seed = self.runner_config.get("seed")
        if seed:
            cmd.extend(("--seed", seed))

        if not self.runner_config["rtp"]:
            cmd.append("--disable-rtp")

        return cmd

    def get_run_data(self):
        cmd = self.get_runner_command()

        if self.default_path:
            game_path = path.expanduser(self.default_path)
            cmd.extend(("--project-path", game_path))

        return {"command": cmd, "env": self.get_env()}

    def play(self):
        if not self.game_path:
            return {"error": "CUSTOM", "text": _("No game directory provided")}
        if not path.isdir(self.game_path):
            return self.directory_not_found(self.game_path)

        cmd = self.get_runner_command()

        cmd.extend(("--project-path", self.game_path))

        encoding = self.game_config.get("encoding")
        if encoding:
            cmd.extend(("--encoding", encoding))

        engine = self.game_config.get("engine")
        if engine:
            cmd.extend(("--engine", engine))

        save_path = self.game_config.get("save_path")
        if save_path:
            save_path = path.expanduser(save_path)
            if not path.isdir(save_path):
                return self.directory_not_found(save_path)
            cmd.extend(("--save-path", save_path))

        record_input = self.game_config.get("record_input")
        if record_input:
            record_input = path.expanduser(record_input)
            cmd.extend(("--record-input", record_input))

        replay_input = self.game_config.get("replay_input")
        if replay_input:
            replay_input = path.expanduser(replay_input)
            if not path.isfile(replay_input):
                return {"error": "FILE_NOT_FOUND", "file": replay_input}
            cmd.extend(("--replay-input", replay_input))

        load_game_id = self.game_config.get("load_game_id")
        if load_game_id:
            cmd.extend(("--load-game-id", str(load_game_id)))

        start_map_id = self.game_config.get("start_map_id")
        if start_map_id:
            cmd.extend(("--start-map-id", str(start_map_id)))

        start_position = self.game_config.get("start_position")
        if start_position:
            cmd.extend(("--start-position", *start_position.split()))

        start_party = self.game_config.get("start_party")
        if start_party:
            cmd.extend(("--start-party", *start_party.split()))

        battle_test = self.game_config.get("battle_test")
        if battle_test:
            cmd.extend(("--battle-test", battle_test))

        return {"command": cmd}

    @staticmethod
    def directory_not_found(directory):
        error = _(
            "The directory {} could not be found"
        ).format(directory.replace("&", "&amp;"))
        return {"error": "CUSTOM", "text": error}
description
download_url
entry_point_option
game_options
game_path property readonly

Return the directory where the game is installed.

human_name
platforms
runnable_alone
runner_executable
runner_options
directory_not_found(directory) staticmethod
Source code in lutris/runners/easyrpg.py
@staticmethod
def directory_not_found(directory):
    error = _(
        "The directory {} could not be found"
    ).format(directory.replace("&", "&amp;"))
    return {"error": "CUSTOM", "text": error}
get_env(self, os_env=False)

Return environment variables used for a game.

Source code in lutris/runners/easyrpg.py
def get_env(self, os_env=False):
    env = super().get_env(os_env)

    rpg2k_rtp_path = self.runner_config.get("rpg2k_rtp_path")
    if rpg2k_rtp_path:
        env["RPG2K_RTP_PATH"] = rpg2k_rtp_path

    rpg2k3_rtp_path = self.runner_config.get("rpg2k3_rtp_path")
    if rpg2k3_rtp_path:
        env["RPG2K3_RTP_PATH"] = rpg2k3_rtp_path

    rpg_rtp_path = self.runner_config.get("rpg_rtp_path")
    if rpg_rtp_path:
        env["RPG_RTP_PATH"] = rpg_rtp_path

    return env
get_run_data(self)

Return dict with command (exe & args list) and env vars (dict).

Reimplement in derived runner if need be.

Source code in lutris/runners/easyrpg.py
def get_run_data(self):
    cmd = self.get_runner_command()

    if self.default_path:
        game_path = path.expanduser(self.default_path)
        cmd.extend(("--project-path", game_path))

    return {"command": cmd, "env": self.get_env()}
get_runner_command(self)
Source code in lutris/runners/easyrpg.py
def get_runner_command(self):
    cmd = [self.get_executable()]

    if self.runner_config["fullscreen"]:
        cmd.append("--fullscreen")
    else:
        cmd.append("--window")

    if not self.runner_config["audio"]:
        cmd.append("--disable-audio")

    if self.runner_config["mouse"]:
        cmd.append("--enable-mouse")

    if self.runner_config["touch"]:
        cmd.append("--enable-touch")

    if self.runner_config["hide_title"]:
        cmd.append("--hide-title")

    if not self.runner_config["vsync"]:
        cmd.append("--no-vsync")

    fps_limit = self.runner_config.get("fps_limit")
    if fps_limit:
        cmd.extend(("--fps-limit", fps_limit))

    show_fps = self.runner_config.get("show_fps")
    if show_fps != "off":
        cmd.append("--show-fps")
    if show_fps == "full":
        cmd.append("--fps-render-window")

    if self.runner_config["test_play"]:
        cmd.append("--test-play")

    seed = self.runner_config.get("seed")
    if seed:
        cmd.extend(("--seed", seed))

    if not self.runner_config["rtp"]:
        cmd.append("--disable-rtp")

    return cmd
play(self)
Source code in lutris/runners/easyrpg.py
def play(self):
    if not self.game_path:
        return {"error": "CUSTOM", "text": _("No game directory provided")}
    if not path.isdir(self.game_path):
        return self.directory_not_found(self.game_path)

    cmd = self.get_runner_command()

    cmd.extend(("--project-path", self.game_path))

    encoding = self.game_config.get("encoding")
    if encoding:
        cmd.extend(("--encoding", encoding))

    engine = self.game_config.get("engine")
    if engine:
        cmd.extend(("--engine", engine))

    save_path = self.game_config.get("save_path")
    if save_path:
        save_path = path.expanduser(save_path)
        if not path.isdir(save_path):
            return self.directory_not_found(save_path)
        cmd.extend(("--save-path", save_path))

    record_input = self.game_config.get("record_input")
    if record_input:
        record_input = path.expanduser(record_input)
        cmd.extend(("--record-input", record_input))

    replay_input = self.game_config.get("replay_input")
    if replay_input:
        replay_input = path.expanduser(replay_input)
        if not path.isfile(replay_input):
            return {"error": "FILE_NOT_FOUND", "file": replay_input}
        cmd.extend(("--replay-input", replay_input))

    load_game_id = self.game_config.get("load_game_id")
    if load_game_id:
        cmd.extend(("--load-game-id", str(load_game_id)))

    start_map_id = self.game_config.get("start_map_id")
    if start_map_id:
        cmd.extend(("--start-map-id", str(start_map_id)))

    start_position = self.game_config.get("start_position")
    if start_position:
        cmd.extend(("--start-position", *start_position.split()))

    start_party = self.game_config.get("start_party")
    if start_party:
        cmd.extend(("--start-party", *start_party.split()))

    battle_test = self.game_config.get("battle_test")
    if battle_test:
        cmd.extend(("--battle-test", battle_test))

    return {"command": cmd}

fsuae

fsuae (Runner)

Source code in lutris/runners/fsuae.py
class fsuae(Runner):
    human_name = _("FS-UAE")
    description = _("Amiga emulator")
    platforms = [
        _("Amiga 500"),
        _("Amiga 500+"),
        _("Amiga 600"),
        _("Amiga 1000"),
        _("Amiga 1200"),
        _("Amiga 1200"),
        _("Amiga 4000"),
        _("Amiga CD32"),
        _("Commodore CDTV"),
    ]
    model_choices = [
        (_("Amiga 500"), "A500"),
        (_("Amiga 500+ with 1 MB chip RAM"), "A500+"),
        (_("Amiga 600 with 1 MB chip RAM"), "A600"),
        (_("Amiga 1000 with 512 KB chip RAM"), "A1000"),
        (_("Amiga 1200 with 2 MB chip RAM"), "A1200"),
        (_("Amiga 1200 but with 68020 processor"), "A1200/020"),
        (_("Amiga 4000 with 2 MB chip RAM and a 68040"), "A4000/040"),
        (_("Amiga CD32"), "CD32"),
        (_("Commodore CDTV"), "CDTV"),
    ]
    cpumodel_choices = [
        (_("68000"), "68000"),
        (_("68010"), "68010"),
        (_("68020 with 24-bit addressing"), "68EC020"),
        (_("68020"), "68020"),
        (_("68030 without internal MMU"), "68EC030"),
        (_("68030"), "68030"),
        (_("68040 without internal FPU and MMU"), "68EC040"),
        (_("68040 without internal FPU"), "68LC040"),
        (_("68040 without internal MMU"), "68040-NOMMU"),
        (_("68040"), "68040"),
        (_("68060 without internal FPU and MMU"), "68EC060"),
        (_("68060 without internal FPU"), "68LC060"),
        (_("68060 without internal MMU"), "68060-NOMMU"),
        (_("68060"), "68060"),
        (_("Auto"), "auto"),
    ]
    memory_choices = [
        (_("0"), "0"),
        (_("1 MB"), "1024"),
        (_("2 MB"), "2048"),
        (_("4 MB"), "4096"),
        (_("8 MB"), "8192"),
    ]
    zorroiii_choices = [
        (_("0"), "0"),
        (_("1 MB"), "1024"),
        (_("2 MB"), "2048"),
        (_("4 MB"), "4096"),
        (_("8 MB"), "8192"),
        (_("16 MB"), "16384"),
        (_("32 MB"), "32768"),
        (_("64 MB"), "65536"),
        (_("128 MB"), "131072"),
        (_("256 MB"), "262144"),
        (_("384 MB"), "393216"),
        (_("512 MB"), "524288"),
        (_("768 MB"), "786432"),
        (_("1 GB"), "1048576"),
    ]
    flsound_choices = [
        ("0", "0"),
        ("25", "25"),
        ("50", "50"),
        ("75", "75"),
        ("100", "100"),
    ]
    gpucard_choices = [
        ("None", "None"),
        ("UAEGFX", "uaegfx"),
        ("UAEGFX Zorro II", "uaegfx-z2"),
        ("UAEGFX Zorro III", "uaegfx-z3"),
        ("Picasso II Zorro II", "picasso-ii"),
        ("Picasso II+ Zorro II", "picasso-ii+"),
        ("Picasso IV", "picasso-iv"),
        ("Picasso IV Zorro II", "picasso-iv-z2"),
        ("Picasso IV Zorro III", "picasso-iv-z3"),
    ]
    gpumem_choices = [
        (_("0"), "0"),
        (_("1 MB"), "1024"),
        (_("2 MB"), "2048"),
        (_("4 MB"), "4096"),
        (_("8 MB"), "8192"),
        (_("16 MB"), "16384"),
        (_("32 MB"), "32768"),
        (_("64 MB"), "65536"),
        (_("128 MB"), "131072"),
        (_("256 MB"), "262144"),
    ]
    flspeed_choices = [
        (_("Turbo"), "0"),
        ("100%", "100"),
        ("200%", "200"),
        ("400%", "400"),
        ("800%", "800"),
    ]
    runner_executable = "fs-uae/fs-uae"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("Boot disk"),
            "default_path": "game_path",
            "help": _(
                "The main floppy disk file with the game data. \n"
                "FS-UAE supports floppy images in multiple file formats: "
                "ADF, IPF, DMS are the most common. ADZ (compressed ADF) "
                "and ADFs in zip files are a also supported.\n"
                "Files ending in .hdf will be mounted as hard drives and "
                "ISOs can be used for Amiga CD32 and CDTV models."
            ),
        }, {
            "option": "disks",
            "type": "multiple",
            "label": _("Additionnal floppies"),
            "default_path": "game_path",
            "help": _("The additional floppy disk image(s)."),
        }, {
            "option": "cdrom_image",
            "label": _("CD-ROM image"),
            "type": "file",
            "help": _("CD-ROM image to use on non CD32/CDTV models")
        }
    ]

    runner_options = [
        {
            "option": "model",
            "label": _("Amiga model"),
            "type": "choice",
            "choices": model_choices,
            "default": "A500",
            "help": _("Specify the Amiga model you want to emulate."),
        },
        {
            "option": "kickstart_file",
            "label": _("Kickstart ROMs location"),
            "type": "file",
            "help": _(
                "Choose the folder containing original Amiga Kickstart "
                "ROMs. Refer to FS-UAE documentation to find how to "
                "acquire them. Without these, FS-UAE uses a bundled "
                "replacement ROM which is less compatible with Amiga "
                "software."
            ),
        },
        {
            "option": "kickstart_ext_file",
            "label": _("Extended Kickstart location"),
            "type": "file",
            "advanced": True,
            "help": _("Location of extended Kickstart used for CD32"),
        },
        {
            "option": "gfx_fullscreen_amiga",
            "label": _("Fullscreen (F12 + S to switch)"),
            "type": "bool",
            "default": False,
        },
        {
            "option": "scanlines",
            "label": _("Scanlines display style"),
            "type": "bool",
            "default": False,
            "help": _("Activates a display filter adding scanlines to imitate "
                      "the displays of yesteryear."),
        },
        {
            "option": "cpumodel",
            "label": _("CPU"),
            "type": "choice",
            "choices": cpumodel_choices,
            "default": "auto",
            "advanced": True,
            "help": _("Use this option to override the CPU model in the emulated Amiga. All Amiga "
                      "models imply a default CPU model, so you only need to use this option if you "
                      "want to use another CPU."),
        },
        {
            "option": "fmemory",
            "label": _("Fast Memory"),
            "type": "choice",
            "choices": memory_choices,
            "default": "0",
            "advanced": True,
            "help": _("Specify how much Fast Memory the Amiga model should have."),
        },
        {
            "option": "ziiimem",
            "label": _("Zorro III RAM"),
            "type": "choice",
            "choices": zorroiii_choices,
            "default": "0",
            "advanced": True,
            "help": _("Override the amount of Zorro III Fast memory, specified in KB. Must be a "
                      "multiple of 1024. The default value depends on [amiga_model]. Requires a "
                      "processor with 32-bit address bus, (use for example the A1200/020 model)."),
        },
        {
            "option": "fdvolume",
            "label": _("Floppy Drive Volume"),
            "type": "choice",
            "choices": flsound_choices,
            "default": "0",
            "advanced": True,
            "help": _("Set volume to 0 to disable floppy drive clicks "
                      "when the drive is empty. Max volume is 100.")
        },
        {
            "option": "fdspeed",
            "label": _("Floppy Drive Speed"),
            "type": "choice",
            "choices": flspeed_choices,
            "default": "100",
            "advanced": True,
            "help": _(
                "Set the speed of the emulated floppy drives, in percent. "
                "For example, you can specify 800 to get an 8x increase in "
                "speed. Use 0 to specify turbo mode. Turbo mode means that "
                "all floppy operations complete immediately. The default is 100 for most models."
            )
        },
        {
            "option": "grafixcard",
            "label": _("Graphics Card"),
            "type": "choice",
            "choices": gpucard_choices,
            "default": "None",
            "advanced": True,
            "help": _(
                "Use this option to enable a graphics card. This option is none by default, in "
                "which case only chipset graphics (OCS/ECS/AGA) support is available."
            )
        },
        {
            "option": "grafixmemory",
            "label": _("Graphics Card RAM"),
            "type": "choice",
            "choices": gpumem_choices,
            "default": "0",
            "advanced": True,
            "help": _(
                "Override the amount of graphics memory on the graphics card. The 0 MB option is "
                "not really valid, but exists for user interface reasons."
            )
        },
        {
            "option": "jitcompiler",
            "label": _("JIT Compiler"),
            "type": "bool",
            "default": False,
            "advanced": True,
        },
        {
            "option": "gamemode",
            "label": _("Feral GameMode"),
            "type": "bool",
            "default": False,
            "advanced": True,
            "help": _("Automatically uses Feral GameMode daemon if available. "
                      "Set to true to disable the feature.")
        },
        {
            "option": "govwarning",
            "label": _("CPU governor warning"),
            "type": "bool",
            "default": False,
            "advanced": True,
            "help":
            _("Warn if running with a CPU governor other than performance. "
              "Set to true to disable the warning.")
        },
        {
            "option": "bsdsocket",
            "label": _("UAE bsdsocket.library"),
            "type": "bool",
            "default": False,
            "advanced": True,
        },
    ]

    def get_platform(self):
        model = self.runner_config.get("model")
        if model:
            for index, machine in enumerate(self.model_choices):
                if machine[1] == model:
                    return self.platforms[index]
        return ""

    def get_absolute_path(self, path):
        """Return the absolute path for a file"""
        return path if os.path.isabs(path) else os.path.join(self.game_path, path)

    def insert_floppies(self):
        disks = []
        main_disk = self.game_config.get("main_file")
        if main_disk:
            disks.append(main_disk)

        game_disks = self.game_config.get("disks") or []
        for disk in game_disks:
            if disk not in disks:
                disks.append(disk)
        # Make all paths absolute
        disks = [self.get_absolute_path(disk) for disk in disks]
        drives = []
        floppy_images = []
        for drive, disk_path in enumerate(disks):
            disk_param = self.get_disk_param(disk_path)
            drives.append("--%s_%d=%s" % (disk_param, drive, disk_path))
            if disk_param == "floppy_drive":
                floppy_images.append("--floppy_image_%d=%s" % (drive, disk_path))
        cdrom_image = self.game_config.get("cdrom_image")
        if cdrom_image:
            drives.append("--cdrom_drive_0=%s" % self.get_absolute_path(cdrom_image))
        return drives + floppy_images

    def get_disk_param(self, disk_path):
        amiga_model = self.runner_config.get("model")
        if amiga_model in ("CD32", "CDTV"):
            return "cdrom_drive"
        if disk_path.lower().endswith(".hdf"):
            return "hard_drive"
        return "floppy_drive"

    def get_params(self):  # pylint: disable=too-many-branches
        params = []
        option_params = {
            "kickstart_file": "--kickstart_file=%s",
            "kickstart_ext_file": "--kickstart_ext_file=%s",
            "model": "--amiga_model=%s",
            "cpumodel": "--cpu=%s",
            "fmemory": "--fast_memory=%s",
            "ziiimem": "--zorro_iii_memory=%s",
            "fdvolume": "--floppy_drive_volume=%s",
            "fdspeed": "--floppy_drive_speed=%s",
            "grafixcard": "--graphics_card=%s",
            "grafixmemory": "--graphics_memory=%s",
        }
        for option, param in option_params.items():
            option_value = self.runner_config.get(option)
            if option_value:
                params.append(param % option_value)

        if self.runner_config.get("gfx_fullscreen_amiga"):
            width = int(DISPLAY_MANAGER.get_current_resolution()[0])
            params.append("--fullscreen")
            # params.append("--fullscreen_mode=fullscreen-window")
            params.append("--fullscreen_mode=fullscreen")
            params.append("--fullscreen_width=%d" % width)
        if self.runner_config.get("jitcompiler"):
            params.append("--jit_compiler=1")
        if self.runner_config.get("bsdsocket"):
            params.append("--bsdsocket_library=1")
        if self.runner_config.get("gamemode"):
            params.append("--game_mode=0")
        if self.runner_config.get("govwarning"):
            params.append("--governor_warning=0")
        if self.runner_config.get("scanlines"):
            params.append("--scanlines=1")
        return params

    def play(self):
        return {"command": [self.get_executable()] + self.get_params() + self.insert_floppies()}
cpumodel_choices
description
flsound_choices
flspeed_choices
game_options
gpucard_choices
gpumem_choices
human_name
memory_choices
model_choices
platforms
runner_executable
runner_options
zorroiii_choices
get_absolute_path(self, path)

Return the absolute path for a file

Source code in lutris/runners/fsuae.py
def get_absolute_path(self, path):
    """Return the absolute path for a file"""
    return path if os.path.isabs(path) else os.path.join(self.game_path, path)
get_disk_param(self, disk_path)
Source code in lutris/runners/fsuae.py
def get_disk_param(self, disk_path):
    amiga_model = self.runner_config.get("model")
    if amiga_model in ("CD32", "CDTV"):
        return "cdrom_drive"
    if disk_path.lower().endswith(".hdf"):
        return "hard_drive"
    return "floppy_drive"
get_params(self)
Source code in lutris/runners/fsuae.py
def get_params(self):  # pylint: disable=too-many-branches
    params = []
    option_params = {
        "kickstart_file": "--kickstart_file=%s",
        "kickstart_ext_file": "--kickstart_ext_file=%s",
        "model": "--amiga_model=%s",
        "cpumodel": "--cpu=%s",
        "fmemory": "--fast_memory=%s",
        "ziiimem": "--zorro_iii_memory=%s",
        "fdvolume": "--floppy_drive_volume=%s",
        "fdspeed": "--floppy_drive_speed=%s",
        "grafixcard": "--graphics_card=%s",
        "grafixmemory": "--graphics_memory=%s",
    }
    for option, param in option_params.items():
        option_value = self.runner_config.get(option)
        if option_value:
            params.append(param % option_value)

    if self.runner_config.get("gfx_fullscreen_amiga"):
        width = int(DISPLAY_MANAGER.get_current_resolution()[0])
        params.append("--fullscreen")
        # params.append("--fullscreen_mode=fullscreen-window")
        params.append("--fullscreen_mode=fullscreen")
        params.append("--fullscreen_width=%d" % width)
    if self.runner_config.get("jitcompiler"):
        params.append("--jit_compiler=1")
    if self.runner_config.get("bsdsocket"):
        params.append("--bsdsocket_library=1")
    if self.runner_config.get("gamemode"):
        params.append("--game_mode=0")
    if self.runner_config.get("govwarning"):
        params.append("--governor_warning=0")
    if self.runner_config.get("scanlines"):
        params.append("--scanlines=1")
    return params
get_platform(self)
Source code in lutris/runners/fsuae.py
def get_platform(self):
    model = self.runner_config.get("model")
    if model:
        for index, machine in enumerate(self.model_choices):
            if machine[1] == model:
                return self.platforms[index]
    return ""
insert_floppies(self)
Source code in lutris/runners/fsuae.py
def insert_floppies(self):
    disks = []
    main_disk = self.game_config.get("main_file")
    if main_disk:
        disks.append(main_disk)

    game_disks = self.game_config.get("disks") or []
    for disk in game_disks:
        if disk not in disks:
            disks.append(disk)
    # Make all paths absolute
    disks = [self.get_absolute_path(disk) for disk in disks]
    drives = []
    floppy_images = []
    for drive, disk_path in enumerate(disks):
        disk_param = self.get_disk_param(disk_path)
        drives.append("--%s_%d=%s" % (disk_param, drive, disk_path))
        if disk_param == "floppy_drive":
            floppy_images.append("--floppy_image_%d=%s" % (drive, disk_path))
    cdrom_image = self.game_config.get("cdrom_image")
    if cdrom_image:
        drives.append("--cdrom_drive_0=%s" % self.get_absolute_path(cdrom_image))
    return drives + floppy_images
play(self)
Source code in lutris/runners/fsuae.py
def play(self):
    return {"command": [self.get_executable()] + self.get_params() + self.insert_floppies()}

hatari

hatari (Runner)

Source code in lutris/runners/hatari.py
class hatari(Runner):
    human_name = _("Hatari")
    description = _("Atari ST computers emulator")
    platforms = [_("Atari ST")]
    runnable_alone = True
    runner_executable = "hatari/bin/hatari"
    entry_point_option = "disk-a"

    game_options = [
        {
            "option":
            "disk-a",
            "type":
            "file",
            "label":
            _("Floppy Disk A"),
            "help": _(
                "Hatari supports floppy disk images in the following "
                "formats: ST, DIM, MSA, STX, IPF, RAW and CRT. The last "
                "three require the caps library (capslib). ZIP is "
                "supported, you don't need to uncompress the file."
            ),
        },
        {
            "option":
            "disk-b",
            "type":
            "file",
            "label":
            _("Floppy Disk B"),
            "help": _(
                "Hatari supports floppy disk images in the following "
                "formats: ST, DIM, MSA, STX, IPF, RAW and CRT. The last "
                "three require the caps library (capslib). ZIP is "
                "supported, you don't need to uncompress the file."
            ),
        },
    ]

    joystick_choices = [(_("None"), "none"), (_("Keyboard"), "keys"), (_("Joystick"), "real")]

    runner_options = [
        {
            "option":
            "bios_file",
            "type":
            "file",
            "label":
            _("Bios file (TOS)"),
            "help": _(
                "TOS is the operating system of the Atari ST "
                "and is necessary to run applications with the best "
                "fidelity, minimizing risks of issues.\n"
                "TOS 1.02 is recommended for games."
            ),
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": False,
        },
        {
            "option": "zoom",
            "type": "bool",
            "label": _("Scale up display by 2 (Atari ST/STE)"),
            "default": True,
            "help": _("Double the screen size in windowed mode."),
        },
        {
            "option":
            "borders",
            "type":
            "bool",
            "label":
            _("Add borders to display"),
            "default":
            False,
            "help": _(
                "Useful for some games and demos using the overscan "
                "technique. The Atari ST displayed borders around the "
                "screen because it was not powerful enough to display "
                "graphics in fullscreen. But people from the demo scene "
                "were able to remove them and some games made use of "
                "this technique."
            ),
        },
        {
            "option":
            "status",
            "type":
            "bool",
            "label":
            _("Display status bar"),
            "default":
            False,
            "help": _(
                "Displays a status bar with some useful information, "
                "like green leds lighting up when the floppy disks are "
                "read."
            ),
        },
        {
            "option": "joy0",
            "type": "choice",
            "label": _("Joystick 1"),
            "choices": joystick_choices,
            "default": "none",
        },
        {
            "option": "joy1",
            "type": "choice",
            "label": _("Joystick 2"),
            "choices": joystick_choices,
            "default": "none",
        },
    ]

    def install(self, version=None, downloader=None, callback=None):

        def on_runner_installed(*args):
            bios_path = system.create_folder("~/.hatari/bios")
            dlg = QuestionDialog(
                {
                    "question": _("Do you want to select an Atari ST BIOS file?"),
                    "title": _("Use BIOS file?"),
                }
            )
            if dlg.result == dlg.YES:
                bios_dlg = FileDialog(_("Select a BIOS file"))
                bios_filename = bios_dlg.filename
                if not bios_filename:
                    return
                shutil.copy(bios_filename, bios_path)
                bios_path = os.path.join(bios_path, os.path.basename(bios_filename))
                config = LutrisConfig(runner_slug="hatari")
                config.raw_runner_config.update({"bios_file": bios_path})
                config.save()
            if callback:
                callback()

        super().install(version=version, downloader=downloader, callback=on_runner_installed)

    def play(self):  # pylint: disable=too-many-branches
        params = [self.get_executable()]
        if self.runner_config.get("fullscreen"):
            params.append("--fullscreen")
        else:
            params.append("--window")

        params.append("--zoom")
        if self.runner_config.get("zoom"):
            params.append("2")
        else:
            params.append("1")

        params.append("--borders")
        if self.runner_config.get("borders"):
            params.append("true")
        else:
            params.append("false")

        params.append("--statusbar")
        if self.runner_config.get("status"):
            params.append("true")
        else:
            params.append("false")

        if self.runner_config.get("joy0"):
            params.append("--joy0")
            params.append(self.runner_config["joy0"])

        if self.runner_config.get("joy1"):
            params.append("--joy1")
            params.append(self.runner_config["joy1"])

        if system.path_exists(self.runner_config.get("bios_file", "")):
            params.append("--tos")
            params.append(self.runner_config["bios_file"])
        else:
            return {"error": "NO_BIOS"}
        diska = self.game_config.get("disk-a")
        if not system.path_exists(diska):
            return {"error": "FILE_NOT_FOUND", "file": diska}
        params.append("--disk-a")
        params.append(diska)

        return {"command": params}
description
entry_point_option
game_options
human_name
joystick_choices
platforms
runnable_alone
runner_executable
runner_options
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/hatari.py
def install(self, version=None, downloader=None, callback=None):

    def on_runner_installed(*args):
        bios_path = system.create_folder("~/.hatari/bios")
        dlg = QuestionDialog(
            {
                "question": _("Do you want to select an Atari ST BIOS file?"),
                "title": _("Use BIOS file?"),
            }
        )
        if dlg.result == dlg.YES:
            bios_dlg = FileDialog(_("Select a BIOS file"))
            bios_filename = bios_dlg.filename
            if not bios_filename:
                return
            shutil.copy(bios_filename, bios_path)
            bios_path = os.path.join(bios_path, os.path.basename(bios_filename))
            config = LutrisConfig(runner_slug="hatari")
            config.raw_runner_config.update({"bios_file": bios_path})
            config.save()
        if callback:
            callback()

    super().install(version=version, downloader=downloader, callback=on_runner_installed)
play(self)
Source code in lutris/runners/hatari.py
def play(self):  # pylint: disable=too-many-branches
    params = [self.get_executable()]
    if self.runner_config.get("fullscreen"):
        params.append("--fullscreen")
    else:
        params.append("--window")

    params.append("--zoom")
    if self.runner_config.get("zoom"):
        params.append("2")
    else:
        params.append("1")

    params.append("--borders")
    if self.runner_config.get("borders"):
        params.append("true")
    else:
        params.append("false")

    params.append("--statusbar")
    if self.runner_config.get("status"):
        params.append("true")
    else:
        params.append("false")

    if self.runner_config.get("joy0"):
        params.append("--joy0")
        params.append(self.runner_config["joy0"])

    if self.runner_config.get("joy1"):
        params.append("--joy1")
        params.append(self.runner_config["joy1"])

    if system.path_exists(self.runner_config.get("bios_file", "")):
        params.append("--tos")
        params.append(self.runner_config["bios_file"])
    else:
        return {"error": "NO_BIOS"}
    diska = self.game_config.get("disk-a")
    if not system.path_exists(diska):
        return {"error": "FILE_NOT_FOUND", "file": diska}
    params.append("--disk-a")
    params.append(diska)

    return {"command": params}

json

Base class and utilities for JSON based runners

JSON_RUNNER_DIRS

JsonRunner (Runner)

Source code in lutris/runners/json.py
class JsonRunner(Runner):
    json_path = None

    def __init__(self, config=None):
        super().__init__(config)
        if not self.json_path:
            raise RuntimeError("Create subclasses of JsonRunner with the json_path attribute set")
        with open(self.json_path, encoding='utf-8') as json_file:
            self._json_data = json.load(json_file)

        self.game_options = self._json_data["game_options"]
        self.runner_options = self._json_data.get("runner_options", [])
        self.human_name = self._json_data["human_name"]
        self.description = self._json_data["description"]
        self.platforms = self._json_data["platforms"]
        self.runner_executable = self._json_data["runner_executable"]
        self.system_options_override = self._json_data.get("system_options_override", [])
        self.entry_point_option = self._json_data.get("entry_point_option", "main_file")
        self.download_url = self._json_data.get("download_url")

    def play(self):
        """Return a launchable command constructed from the options"""
        arguments = [self.get_executable()]
        for option in self.runner_options:
            if option["option"] not in self.runner_config:
                continue
            if option["type"] == "bool":
                if self.runner_config.get(option["option"]):
                    arguments.append(option["argument"])
            elif option["type"] == "choice":
                if self.runner_config.get(option["option"]) != "off":
                    arguments.append(option["argument"])
                    arguments.append(self.runner_config.get(option["option"]))
            else:
                raise RuntimeError("Unhandled type %s" % option["type"])
        main_file = self.game_config.get(self.entry_point_option)
        if not system.path_exists(main_file):
            return {"error": "FILE_NOT_FOUND", "file": main_file}
        arguments.append(main_file)
        return {"command": arguments}
json_path
__init__(self, config=None) special
Source code in lutris/runners/json.py
def __init__(self, config=None):
    super().__init__(config)
    if not self.json_path:
        raise RuntimeError("Create subclasses of JsonRunner with the json_path attribute set")
    with open(self.json_path, encoding='utf-8') as json_file:
        self._json_data = json.load(json_file)

    self.game_options = self._json_data["game_options"]
    self.runner_options = self._json_data.get("runner_options", [])
    self.human_name = self._json_data["human_name"]
    self.description = self._json_data["description"]
    self.platforms = self._json_data["platforms"]
    self.runner_executable = self._json_data["runner_executable"]
    self.system_options_override = self._json_data.get("system_options_override", [])
    self.entry_point_option = self._json_data.get("entry_point_option", "main_file")
    self.download_url = self._json_data.get("download_url")
play(self)

Return a launchable command constructed from the options

Source code in lutris/runners/json.py
def play(self):
    """Return a launchable command constructed from the options"""
    arguments = [self.get_executable()]
    for option in self.runner_options:
        if option["option"] not in self.runner_config:
            continue
        if option["type"] == "bool":
            if self.runner_config.get(option["option"]):
                arguments.append(option["argument"])
        elif option["type"] == "choice":
            if self.runner_config.get(option["option"]) != "off":
                arguments.append(option["argument"])
                arguments.append(self.runner_config.get(option["option"]))
        else:
            raise RuntimeError("Unhandled type %s" % option["type"])
    main_file = self.game_config.get(self.entry_point_option)
    if not system.path_exists(main_file):
        return {"error": "FILE_NOT_FOUND", "file": main_file}
    arguments.append(main_file)
    return {"command": arguments}

load_json_runners()

Source code in lutris/runners/json.py
def load_json_runners():
    json_runners = {}
    for json_dir in JSON_RUNNER_DIRS:
        if not os.path.exists(json_dir):
            continue
        for json_path in os.listdir(json_dir):
            if not json_path.endswith(".json"):
                continue
            runner_name = json_path[:-5]
            runner_class = type(
                runner_name,
                (JsonRunner, ),
                {'json_path': os.path.join(json_dir, json_path)}
            )
            json_runners[runner_name] = runner_class
    return json_runners

jzintv

jzintv (Runner)

Source code in lutris/runners/jzintv.py
class jzintv(Runner):
    human_name = _("jzIntv")
    description = _("Intellivision Emulator")
    platforms = [_("Intellivision")]
    runner_executable = "jzintv/bin/jzintv"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "default_path": "game_path",
            "help": _(
                "The game data, commonly called a ROM image. \n"
                "Supported formats: ROM, BIN+CFG, INT, ITV \n"
                "The file extension must be lower-case."
            ),
        }
    ]
    runner_options = [
        {
            "option": "bios_path",
            "type": "directory_chooser",
            "label": _("Bios location"),
            "help": _(
                "Choose the folder containing the Intellivision BIOS "
                "files (exec.bin and grom.bin).\n"
                "These files contain code from the original hardware "
                "necessary to the emulation."
            ),
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen")
        },
        {
            "option": "resolution",
            "type": "choice",
            "label": _("Resolution"),
            "choices": (
                ("320 x 200", "0"),
                ("640 x 480", "1"),
                ("800 x 400", "5"),
                ("800 x 600", "2"),
                ("1024 x 768", "3"),
                ("1680 x 1050", "4"),
                ("1600 x 1200", "6"),
            ),
            "default": "0"
        },
    ]

    def play(self):
        """Run Intellivision game"""
        arguments = [self.get_executable()]

        selected_resolution = self.runner_config.get("resolution")
        if selected_resolution:
            arguments = arguments + ["-z%s" % selected_resolution]

        if self.runner_config.get("fullscreen"):
            arguments = arguments + ["-f"]

        bios_path = self.runner_config.get("bios_path", "")
        if system.path_exists(bios_path):
            arguments.append("--execimg=%s/exec.bin" % bios_path)
            arguments.append("--gromimg=%s/grom.bin" % bios_path)
        else:
            return {"error": "NO_BIOS"}
        rom_path = self.game_config.get("main_file") or ""
        if not system.path_exists(rom_path):
            return {"error": "FILE_NOT_FOUND", "file": rom_path}
        romdir = os.path.dirname(rom_path)
        romfile = os.path.basename(rom_path)
        arguments += ["--rom-path=%s/" % romdir]
        arguments += [romfile]
        return {"command": arguments}
description
game_options
human_name
platforms
runner_executable
runner_options
play(self)

Run Intellivision game

Source code in lutris/runners/jzintv.py
def play(self):
    """Run Intellivision game"""
    arguments = [self.get_executable()]

    selected_resolution = self.runner_config.get("resolution")
    if selected_resolution:
        arguments = arguments + ["-z%s" % selected_resolution]

    if self.runner_config.get("fullscreen"):
        arguments = arguments + ["-f"]

    bios_path = self.runner_config.get("bios_path", "")
    if system.path_exists(bios_path):
        arguments.append("--execimg=%s/exec.bin" % bios_path)
        arguments.append("--gromimg=%s/grom.bin" % bios_path)
    else:
        return {"error": "NO_BIOS"}
    rom_path = self.game_config.get("main_file") or ""
    if not system.path_exists(rom_path):
        return {"error": "FILE_NOT_FOUND", "file": rom_path}
    romdir = os.path.dirname(rom_path)
    romfile = os.path.basename(rom_path)
    arguments += ["--rom-path=%s/" % romdir]
    arguments += [romfile]
    return {"command": arguments}

libretro

libretro runner

LIBRETRO_CORES

libretro (Runner)

Source code in lutris/runners/libretro.py
class libretro(Runner):
    human_name = _("Libretro")
    description = _("Multi-system emulator")
    runnable_alone = True
    runner_executable = "retroarch/retroarch"

    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file")
        },
        {
            "option": "core",
            "type": "choice",
            "label": _("Core"),
            "choices": get_core_choices(),
        },
    ]

    runner_options = [
        {
            "option": "config_file",
            "type": "file",
            "label": _("Config file"),
            "default": get_default_config_path("retroarch.cfg"),
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": True,
        },
        {
            "option": "verbose",
            "type": "bool",
            "label": _("Verbose logging"),
            "default": False,
        },
    ]

    @property
    def platforms(self):
        return [core[2] for core in LIBRETRO_CORES]

    def get_platform(self):
        game_core = self.game_config.get("core")
        if not game_core:
            logger.warning("Game don't have a core set")
            return
        for core in LIBRETRO_CORES:
            if core[1] == game_core:
                return core[2]
        logger.warning("'%s' not found in Libretro cores", game_core)
        return ""

    def get_core_path(self, core):
        """Return the path of a core, prioritizing Retroarch cores"""
        lutris_cores_folder = os.path.join(settings.RUNNER_DIR, "retroarch", "cores")
        retroarch_core_folder = os.path.join(os.path.expanduser("~/.config/retroarch/cores"))
        core_filename = "{}_libretro.so".format(core)
        retroarch_core = os.path.join(retroarch_core_folder, core_filename)
        if system.path_exists(retroarch_core):
            return retroarch_core
        return os.path.join(lutris_cores_folder, core_filename)

    def get_version(self, use_default=True):
        return self.game_config["core"]

    def is_retroarch_installed(self):
        return system.path_exists(self.get_executable())

    def is_installed(self, core=None):
        if not core and self.game_config.get("core"):
            core = self.game_config["core"]
        if not core or self.runner_config.get("runner_executable"):
            return self.is_retroarch_installed()
        is_core_installed = system.path_exists(self.get_core_path(core))
        return self.is_retroarch_installed() and is_core_installed

    def install(self, version=None, downloader=None, callback=None):
        captured_super = super()  # super() does not work inside install_core()

        def install_core():
            if not version:
                if callback:
                    callback()
            else:
                captured_super.install(version, downloader, callback)

        if not self.is_retroarch_installed():
            captured_super.install(version=None, downloader=downloader, callback=install_core)
        else:
            captured_super.install(version, downloader, callback)

    def get_run_data(self):
        return {
            "command": [self.get_executable()] + self.get_runner_parameters(),
            "env": self.get_env(),
        }

    def get_config_file(self):
        return self.runner_config.get("config_file") or get_default_config_path("retroarch.cfg")

    @staticmethod
    def get_system_directory(retro_config):
        """Return the system directory used for storing BIOS and firmwares."""
        system_directory = retro_config["system_directory"]
        if not system_directory or system_directory == "default":
            system_directory = get_default_config_path("system")
        return os.path.expanduser(system_directory)

    def prelaunch(self):
        # pylint: disable=too-many-locals,too-many-branches,too-many-statements
        config_file = self.get_config_file()
        # TODO: review later
        # Create retroarch.cfg if it doesn't exist.
        if not system.path_exists(config_file):
            with open(config_file, "w", encoding='utf-8') as f:
                f.write("# Lutris RetroArch Configuration")
                f.close()

            # Build the default config settings.
            retro_config = RetroConfig(config_file)
            retro_config["libretro_directory"] = get_default_config_path("cores")
            retro_config["libretro_info_path"] = get_default_config_path("info")
            retro_config["content_database_path"] = get_default_config_path("database/rdb")
            retro_config["cheat_database_path"] = get_default_config_path("database/cht")
            retro_config["cursor_directory"] = get_default_config_path("database/cursors")
            retro_config["screenshot_directory"] = get_default_config_path("screenshots")
            retro_config["input_remapping_directory"] = get_default_config_path("remaps")
            retro_config["video_shader_dir"] = get_default_config_path("shaders")
            retro_config["core_assets_directory"] = get_default_config_path("downloads")
            retro_config["thumbnails_directory"] = get_default_config_path("thumbnails")
            retro_config["playlist_directory"] = get_default_config_path("playlists")
            retro_config["joypad_autoconfig_dir"] = get_default_config_path("autoconfig")
            retro_config["rgui_config_directory"] = get_default_config_path("config")
            retro_config["overlay_directory"] = get_default_config_path("overlay")
            retro_config["assets_directory"] = get_default_config_path("assets")
            retro_config.save()
        else:
            retro_config = RetroConfig(config_file)

        core = self.game_config.get("core")
        info_file = os.path.join(get_default_config_path("info"), "{}_libretro.info".format(core))
        if system.path_exists(info_file):
            retro_config = RetroConfig(info_file)
            try:
                firmware_count = int(retro_config["firmware_count"])
            except (ValueError, TypeError):
                firmware_count = 0
            system_path = self.get_system_directory(retro_config)
            notes = str(retro_config["notes"] or "")
            checksums = {}
            if notes.startswith("Suggested md5sums:"):
                parts = notes.split("|")
                for part in parts[1:]:
                    checksum, filename = part.split(" = ")
                    checksums[filename] = checksum
            for index in range(firmware_count):
                firmware_filename = retro_config["firmware%d_path" % index]
                firmware_path = os.path.join(system_path, firmware_filename)
                if system.path_exists(firmware_path):
                    if firmware_filename in checksums:
                        checksum = system.get_md5_hash(firmware_path)
                        if checksum == checksums[firmware_filename]:
                            checksum_status = "Checksum good"
                        else:
                            checksum_status = "Checksum failed"
                    else:
                        checksum_status = "No checksum info"
                    logger.info("Firmware '%s' found (%s)", firmware_filename, checksum_status)
                else:
                    logger.warning("Firmware '%s' not found!", firmware_filename)

                # Before closing issue #431
                # TODO check for firmware*_opt and display an error message if
                # firmware is missing
                # TODO Add dialog for copying the firmware in the correct
                # location

        return True

    def get_runner_parameters(self):
        parameters = []

        # Fullscreen
        fullscreen = self.runner_config.get("fullscreen")
        if fullscreen:
            parameters.append("--fullscreen")

        # Verbose
        verbose = self.runner_config.get("verbose")
        if verbose:
            parameters.append("--verbose")

        parameters.append("--config={}".format(self.get_config_file()))
        return parameters

    def play(self):
        command = [self.get_executable()]

        command += self.get_runner_parameters()

        # Core
        core = self.game_config.get("core")
        if not core:
            return {
                "error": "CUSTOM",
                "text": _("No core has been selected for this game"),
            }
        command.append("--libretro={}".format(self.get_core_path(core)))

        # Ensure the core is available
        if not self.is_installed(core):
            self.install(core)

        # Main file
        file = self.game_config.get("main_file")
        if not file:
            return {"error": "CUSTOM", "text": _("No game file specified")}
        if not system.path_exists(file):
            return {"error": "FILE_NOT_FOUND", "file": file}
        command.append(file)
        return {"command": command}

    # Checks whether the retroarch or libretro directories can be uninstalled.
    def can_uninstall(self):
        retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
        return os.path.isdir(retroarch_path) or super().can_uninstall()

    # Remove the `retroarch` directory.
    def uninstall(self):
        retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
        if os.path.isdir(retroarch_path):
            system.remove_folder(retroarch_path)
        super().uninstall()
description
game_options
human_name
platforms property readonly

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

runnable_alone
runner_executable
runner_options
can_uninstall(self)
Source code in lutris/runners/libretro.py
def can_uninstall(self):
    retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
    return os.path.isdir(retroarch_path) or super().can_uninstall()
get_config_file(self)
Source code in lutris/runners/libretro.py
def get_config_file(self):
    return self.runner_config.get("config_file") or get_default_config_path("retroarch.cfg")
get_core_path(self, core)

Return the path of a core, prioritizing Retroarch cores

Source code in lutris/runners/libretro.py
def get_core_path(self, core):
    """Return the path of a core, prioritizing Retroarch cores"""
    lutris_cores_folder = os.path.join(settings.RUNNER_DIR, "retroarch", "cores")
    retroarch_core_folder = os.path.join(os.path.expanduser("~/.config/retroarch/cores"))
    core_filename = "{}_libretro.so".format(core)
    retroarch_core = os.path.join(retroarch_core_folder, core_filename)
    if system.path_exists(retroarch_core):
        return retroarch_core
    return os.path.join(lutris_cores_folder, core_filename)
get_platform(self)
Source code in lutris/runners/libretro.py
def get_platform(self):
    game_core = self.game_config.get("core")
    if not game_core:
        logger.warning("Game don't have a core set")
        return
    for core in LIBRETRO_CORES:
        if core[1] == game_core:
            return core[2]
    logger.warning("'%s' not found in Libretro cores", game_core)
    return ""
get_run_data(self)

Return dict with command (exe & args list) and env vars (dict).

Reimplement in derived runner if need be.

Source code in lutris/runners/libretro.py
def get_run_data(self):
    return {
        "command": [self.get_executable()] + self.get_runner_parameters(),
        "env": self.get_env(),
    }
get_runner_parameters(self)
Source code in lutris/runners/libretro.py
def get_runner_parameters(self):
    parameters = []

    # Fullscreen
    fullscreen = self.runner_config.get("fullscreen")
    if fullscreen:
        parameters.append("--fullscreen")

    # Verbose
    verbose = self.runner_config.get("verbose")
    if verbose:
        parameters.append("--verbose")

    parameters.append("--config={}".format(self.get_config_file()))
    return parameters
get_system_directory(retro_config) staticmethod

Return the system directory used for storing BIOS and firmwares.

Source code in lutris/runners/libretro.py
@staticmethod
def get_system_directory(retro_config):
    """Return the system directory used for storing BIOS and firmwares."""
    system_directory = retro_config["system_directory"]
    if not system_directory or system_directory == "default":
        system_directory = get_default_config_path("system")
    return os.path.expanduser(system_directory)
get_version(self, use_default=True)
Source code in lutris/runners/libretro.py
def get_version(self, use_default=True):
    return self.game_config["core"]
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/libretro.py
def install(self, version=None, downloader=None, callback=None):
    captured_super = super()  # super() does not work inside install_core()

    def install_core():
        if not version:
            if callback:
                callback()
        else:
            captured_super.install(version, downloader, callback)

    if not self.is_retroarch_installed():
        captured_super.install(version=None, downloader=downloader, callback=install_core)
    else:
        captured_super.install(version, downloader, callback)
is_installed(self, core=None)

Return whether the runner is installed

Source code in lutris/runners/libretro.py
def is_installed(self, core=None):
    if not core and self.game_config.get("core"):
        core = self.game_config["core"]
    if not core or self.runner_config.get("runner_executable"):
        return self.is_retroarch_installed()
    is_core_installed = system.path_exists(self.get_core_path(core))
    return self.is_retroarch_installed() and is_core_installed
is_retroarch_installed(self)
Source code in lutris/runners/libretro.py
def is_retroarch_installed(self):
    return system.path_exists(self.get_executable())
play(self)
Source code in lutris/runners/libretro.py
def play(self):
    command = [self.get_executable()]

    command += self.get_runner_parameters()

    # Core
    core = self.game_config.get("core")
    if not core:
        return {
            "error": "CUSTOM",
            "text": _("No core has been selected for this game"),
        }
    command.append("--libretro={}".format(self.get_core_path(core)))

    # Ensure the core is available
    if not self.is_installed(core):
        self.install(core)

    # Main file
    file = self.game_config.get("main_file")
    if not file:
        return {"error": "CUSTOM", "text": _("No game file specified")}
    if not system.path_exists(file):
        return {"error": "FILE_NOT_FOUND", "file": file}
    command.append(file)
    return {"command": command}
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/libretro.py
def prelaunch(self):
    # pylint: disable=too-many-locals,too-many-branches,too-many-statements
    config_file = self.get_config_file()
    # TODO: review later
    # Create retroarch.cfg if it doesn't exist.
    if not system.path_exists(config_file):
        with open(config_file, "w", encoding='utf-8') as f:
            f.write("# Lutris RetroArch Configuration")
            f.close()

        # Build the default config settings.
        retro_config = RetroConfig(config_file)
        retro_config["libretro_directory"] = get_default_config_path("cores")
        retro_config["libretro_info_path"] = get_default_config_path("info")
        retro_config["content_database_path"] = get_default_config_path("database/rdb")
        retro_config["cheat_database_path"] = get_default_config_path("database/cht")
        retro_config["cursor_directory"] = get_default_config_path("database/cursors")
        retro_config["screenshot_directory"] = get_default_config_path("screenshots")
        retro_config["input_remapping_directory"] = get_default_config_path("remaps")
        retro_config["video_shader_dir"] = get_default_config_path("shaders")
        retro_config["core_assets_directory"] = get_default_config_path("downloads")
        retro_config["thumbnails_directory"] = get_default_config_path("thumbnails")
        retro_config["playlist_directory"] = get_default_config_path("playlists")
        retro_config["joypad_autoconfig_dir"] = get_default_config_path("autoconfig")
        retro_config["rgui_config_directory"] = get_default_config_path("config")
        retro_config["overlay_directory"] = get_default_config_path("overlay")
        retro_config["assets_directory"] = get_default_config_path("assets")
        retro_config.save()
    else:
        retro_config = RetroConfig(config_file)

    core = self.game_config.get("core")
    info_file = os.path.join(get_default_config_path("info"), "{}_libretro.info".format(core))
    if system.path_exists(info_file):
        retro_config = RetroConfig(info_file)
        try:
            firmware_count = int(retro_config["firmware_count"])
        except (ValueError, TypeError):
            firmware_count = 0
        system_path = self.get_system_directory(retro_config)
        notes = str(retro_config["notes"] or "")
        checksums = {}
        if notes.startswith("Suggested md5sums:"):
            parts = notes.split("|")
            for part in parts[1:]:
                checksum, filename = part.split(" = ")
                checksums[filename] = checksum
        for index in range(firmware_count):
            firmware_filename = retro_config["firmware%d_path" % index]
            firmware_path = os.path.join(system_path, firmware_filename)
            if system.path_exists(firmware_path):
                if firmware_filename in checksums:
                    checksum = system.get_md5_hash(firmware_path)
                    if checksum == checksums[firmware_filename]:
                        checksum_status = "Checksum good"
                    else:
                        checksum_status = "Checksum failed"
                else:
                    checksum_status = "No checksum info"
                logger.info("Firmware '%s' found (%s)", firmware_filename, checksum_status)
            else:
                logger.warning("Firmware '%s' not found!", firmware_filename)

            # Before closing issue #431
            # TODO check for firmware*_opt and display an error message if
            # firmware is missing
            # TODO Add dialog for copying the firmware in the correct
            # location

    return True
uninstall(self)
Source code in lutris/runners/libretro.py
def uninstall(self):
    retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
    if os.path.isdir(retroarch_path):
        system.remove_folder(retroarch_path)
    super().uninstall()

get_core_choices()

Source code in lutris/runners/libretro.py
def get_core_choices():
    return [(core[0], core[1]) for core in LIBRETRO_CORES]

get_default_config_path(path='')

Source code in lutris/runners/libretro.py
def get_default_config_path(path=""):
    return os.path.join(settings.RUNNER_DIR, "retroarch", path)

get_libretro_cores()

Source code in lutris/runners/libretro.py
def get_libretro_cores():
    cores = []
    runner_path = get_default_config_path()
    if not os.path.exists(runner_path):
        logger.warning("No folder at %s", runner_path)
        return []

    # Get core identifiers from info dir
    info_path = get_default_config_path("info")
    if not os.path.exists(info_path):
        req = requests.get("http://buildbot.libretro.com/assets/frontend/info.zip", allow_redirects=True)
        if req.status_code == requests.codes.ok:  # pylint: disable=no-member
            with open(get_default_config_path('info.zip'), 'wb') as info_zip:
                info_zip.write(req.content)
            with ZipFile(get_default_config_path('info.zip'), 'r') as info_zip:
                info_zip.extractall(info_path)
        else:
            logger.error("Error retrieving libretro info archive from server: %s - %s", req.status_code, req.reason)
            return []
    # Parse info files to fetch display name and platform/system
    for info_file in os.listdir(info_path):
        if "_libretro.info" not in info_file:
            continue
        core_identifier = info_file.replace("_libretro.info", "")
        core_config = RetroConfig(os.path.join(info_path, info_file))
        if "categories" in core_config.keys() and "Emulator" in core_config["categories"]:
            core_label = core_config["display_name"] or ""
            core_system = core_config["systemname"] or ""
            cores.append((core_label, core_identifier, core_system))
    cores.sort(key=itemgetter(0))
    return cores

linux

Runner for Linux games

linux (Runner)

Source code in lutris/runners/linux.py
class linux(Runner):
    human_name = _("Linux")
    description = _("Runs native games")
    platforms = [_("Linux")]
    entry_point_option = "exe"

    game_options = [
        {
            "option": "exe",
            "type": "file",
            "default_path": "game_path",
            "label": _("Executable"),
            "help": _("The game's main executable file"),
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "help": _("Command line arguments used when launching the game"),
        },
        {
            "option":
            "working_dir",
            "type":
            "directory_chooser",
            "label":
            _("Working directory"),
            "help": _(
                "The location where the game is run from.\n"
                "By default, Lutris uses the directory of the "
                "executable."
            ),
        },
        {
            "option": "ld_preload",
            "type": "file",
            "label": _("Preload library"),
            "advanced": True,
            "help": _("A library to load before running the game's executable."),
        },
        {
            "option":
            "ld_library_path",
            "type":
            "directory_chooser",
            "label":
            _("Add directory to LD_LIBRARY_PATH"),
            "advanced":
            True,
            "help": _(
                "A directory where libraries should be searched for "
                "first, before the standard set of directories; this is "
                "useful when debugging a new library or using a "
                "nonstandard library for special purposes."
            ),
        },
    ]

    def __init__(self, config=None):
        super().__init__(config)
        self.ld_preload = None

    @property
    def game_exe(self):
        """Return the game's executable's path. The file may not exist, but
        this returns None if the exe path is not defined."""
        exe = self.game_config.get("exe")
        if not exe:
            return None
        if os.path.isabs(exe):
            return exe
        if self.game_path:
            return os.path.join(self.game_path, exe)
        return system.find_executable(exe)

    def get_relative_exe(self):
        """Return a relative path if a working dir is set in the options
        Some games such as Unreal Gold fail to run if given the absolute path
        """
        exe_path = self.game_exe
        working_dir = self.game_config.get("working_dir")
        if exe_path and working_dir:
            parts = exe_path.split(os.path.expanduser(working_dir))
            if len(parts) == 2:
                return "." + parts[1]
        return exe_path

    @property
    def working_dir(self):
        """Return the working directory to use when running the game."""
        option = self.game_config.get("working_dir")
        if option:
            return os.path.expanduser(option)
        if self.game_exe:
            return os.path.dirname(self.game_exe)
        return super().working_dir

    @property
    def nvidia_shader_cache_path(self):
        """Linux programs should get individual shader caches if possible."""
        return self.game_path or self.shader_cache_dir

    def is_installed(self):
        """Well of course Linux is installed, you're using Linux right ?"""
        return True

    def play(self):
        """Run native game."""
        launch_info = {}

        if not self.game_exe or not system.path_exists(self.game_exe):
            return {"error": "FILE_NOT_FOUND", "file": self.game_exe}

        # Quit if the file is not executable
        mode = os.stat(self.game_exe).st_mode
        if not mode & stat.S_IXUSR:
            return {"error": "NOT_EXECUTABLE", "file": self.game_exe}

        if not system.path_exists(self.game_exe):
            return {"error": "FILE_NOT_FOUND", "file": self.game_exe}

        ld_preload = self.game_config.get("ld_preload")
        if ld_preload:
            launch_info["ld_preload"] = ld_preload

        ld_library_path = self.game_config.get("ld_library_path")
        if ld_library_path:
            launch_info["ld_library_path"] = os.path.expanduser(ld_library_path)

        command = [self.get_relative_exe()]

        args = self.game_config.get("args") or ""
        for arg in split_arguments(args):
            command.append(arg)
        launch_info["command"] = command
        return launch_info
description
entry_point_option
game_exe property readonly

Return the game's executable's path. The file may not exist, but this returns None if the exe path is not defined.

game_options
human_name
nvidia_shader_cache_path property readonly

Linux programs should get individual shader caches if possible.

platforms
working_dir property readonly

Return the working directory to use when running the game.

__init__(self, config=None) special
Source code in lutris/runners/linux.py
def __init__(self, config=None):
    super().__init__(config)
    self.ld_preload = None
get_relative_exe(self)

Return a relative path if a working dir is set in the options Some games such as Unreal Gold fail to run if given the absolute path

Source code in lutris/runners/linux.py
def get_relative_exe(self):
    """Return a relative path if a working dir is set in the options
    Some games such as Unreal Gold fail to run if given the absolute path
    """
    exe_path = self.game_exe
    working_dir = self.game_config.get("working_dir")
    if exe_path and working_dir:
        parts = exe_path.split(os.path.expanduser(working_dir))
        if len(parts) == 2:
            return "." + parts[1]
    return exe_path
is_installed(self)

Well of course Linux is installed, you're using Linux right ?

Source code in lutris/runners/linux.py
def is_installed(self):
    """Well of course Linux is installed, you're using Linux right ?"""
    return True
play(self)

Run native game.

Source code in lutris/runners/linux.py
def play(self):
    """Run native game."""
    launch_info = {}

    if not self.game_exe or not system.path_exists(self.game_exe):
        return {"error": "FILE_NOT_FOUND", "file": self.game_exe}

    # Quit if the file is not executable
    mode = os.stat(self.game_exe).st_mode
    if not mode & stat.S_IXUSR:
        return {"error": "NOT_EXECUTABLE", "file": self.game_exe}

    if not system.path_exists(self.game_exe):
        return {"error": "FILE_NOT_FOUND", "file": self.game_exe}

    ld_preload = self.game_config.get("ld_preload")
    if ld_preload:
        launch_info["ld_preload"] = ld_preload

    ld_library_path = self.game_config.get("ld_library_path")
    if ld_library_path:
        launch_info["ld_library_path"] = os.path.expanduser(ld_library_path)

    command = [self.get_relative_exe()]

    args = self.game_config.get("args") or ""
    for arg in split_arguments(args):
        command.append(arg)
    launch_info["command"] = command
    return launch_info

mame

Runner for MAME

MAME_CACHE_DIR

MAME_XML_PATH

mame (Runner)

MAME runner

Source code in lutris/runners/mame.py
class mame(Runner):  # pylint: disable=invalid-name

    """MAME runner"""

    human_name = _("MAME")
    description = _("Arcade game emulator")
    runner_executable = "mame/mame"
    runnable_alone = True
    config_dir = os.path.expanduser("~/.mame")
    cache_dir = os.path.join(settings.CACHE_DIR, "mame")
    xml_path = os.path.join(cache_dir, "mame.xml")
    _platforms = []

    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
        },
        {
            "option": "machine",
            "type": "choice_with_search",
            "label": _("Machine"),
            "choices": get_system_choices,
            "help": _("The emulated machine.")
        },
        {
            "option": "device",
            "type": "choice_with_entry",
            "label": _("Storage type"),
            "choices": [
                (_("Floppy disk"), "flop"),
                (_("Floppy drive 1"), "flop1"),
                (_("Floppy drive 2"), "flop2"),
                (_("Floppy drive 3"), "flop3"),
                (_("Floppy drive 4"), "flop4"),
                (_("Cassette (tape)"), "cass"),
                (_("Cassette 1 (tape)"), "cass1"),
                (_("Cassette 2 (tape)"), "cass2"),
                (_("Cartridge"), "cart"),
                (_("Cartridge 1"), "cart1"),
                (_("Cartridge 2"), "cart2"),
                (_("Cartridge 3"), "cart3"),
                (_("Cartridge 4"), "cart4"),
                (_("Snapshot"), "snapshot"),
                (_("Hard Disk"), "hard"),
                (_("Hard Disk 1"), "hard1"),
                (_("Hard Disk 2"), "hard2"),
                (_("CD-ROM"), "cdrm"),
                (_("CD-ROM 1"), "cdrm1"),
                (_("CD-ROM 2"), "cdrm2"),
                (_("Snapshot"), "dump"),
                (_("Quickload"), "quickload"),
                (_("Memory Card"), "memc"),
                (_("Cylinder"), "cyln"),
                (_("Punch Tape 1"), "ptap1"),
                (_("Punch Tape 2"), "ptap2"),
                (_("Print Out"), "prin"),
            ],
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "help": _("Command line arguments used when launching the game"),
        },
        {
            "option": "autoboot_command",
            "type": "string",
            "label": _("Autoboot command"),
            "help": _("Autotype this command when the system has started, "
                      "an enter keypress is automatically added."),
        },
        {
            "option": "autoboot_delay",
            "type": "range",
            "label": _("Delay before entering autoboot command"),
            "min": 0,
            "max": 120,
        }
    ]

    runner_options = [
        {
            "option": "rompath",
            "type": "directory_chooser",
            "label": _("ROM/BIOS path"),
            "help": _(
                "Choose the folder containing ROMs and BIOS files.\n"
                "These files contain code from the original hardware "
                "necessary to the emulation."
            ),
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": True,
        },
        {
            "option": "crt",
            "type": "bool",
            "label": _("CRT effect ()"),
            "help": _("Applies a CRT effect to the screen."
                      "Requires OpenGL renderer."),
            "default": False,
        },
        {
            "option": "video",
            "type": "choice",
            "label": _("Video backend"),
            "choices": (
                (_("Auto"), ""),
                ("OpenGL", "opengl"),
                ("BGFX", "bgfx"),
                ("SDL2", "accel"),
                (_("Software"), "soft"),
            ),
            "default": "opengl",
        },
        {
            "option": "waitvsync",
            "type": "bool",
            "label": _("Wait for VSync"),
            "help":
            _("Enable waiting for  the  start  of  vblank  before "
              "flipping  screens; reduces tearing effects."),
            "advanced": True,
            "default": False,
        },
        {
            "option": "uimodekey",
            "type": "choice_with_entry",
            "label": _("Menu mode key"),
            "choices": [
                (_("Scroll Lock"), "SCRLOCK"),
                (_("Num Lock"), "NUMLOCK"),
                (_("Caps Lock"), "CAPSLOCK"),
                (_("Menu"), "MENU"),
                (_("Right Control"), "RCONTROL"),
                (_("Left Control"), "LCONTROL"),
                (_("Right Alt"), "RALT"),
                (_("Left Alt"), "LALT"),
                (_("Right Super"), "RWIN"),
                (_("Left Super"), "LWIN"),
            ],
            "default": "SCRLOCK",
            "advanced": True,
            "help": _("Key to switch between Full Keyboard Mode and "
                      "Partial Keyboard Mode (default: Scroll Lock)"),
        },
    ]

    @property
    def working_dir(self):
        return os.path.join(settings.RUNNER_DIR, "mame")

    @property
    def platforms(self):
        if self._platforms:
            return self.platforms
        self._platforms = [choice[0] for choice in get_system_choices(include_year=False)]
        self._platforms += [_("Arcade"), _("Nintendo Game & Watch")]
        return self._platforms

    def install(self, version=None, downloader=None, callback=None):

        def on_runner_installed(*args):
            AsyncCall(write_mame_xml, notify_mame_xml)

        super().install(version=version, downloader=downloader, callback=on_runner_installed)

    @property
    def default_path(self):
        """Return the default path, use the runner's rompath"""
        main_file = self.game_config.get("main_file")
        if main_file:
            return os.path.dirname(main_file)
        return self.runner_config.get("rompath")

    def write_xml_list(self):
        """Write the full game list in XML to disk"""
        os.makedirs(self.cache_dir, exist_ok=True)
        output = system.execute(
            [self.get_executable(), "-listxml"],
            env=runtime.get_env()
        )
        if output:
            with open(self.xml_path, "w", encoding='utf-8') as xml_file:
                xml_file.write(output)
            logger.info("MAME XML list written to %s", self.xml_path)
        else:
            logger.warning("Couldn't get any output for mame -listxml")

    def get_platform(self):
        selected_platform = self.game_config.get("platform")
        if selected_platform:
            return self.platforms[int(selected_platform)]
        if self.game_config.get("machine"):
            machine_mapping = {choice[1]: choice[0] for choice in get_system_choices(include_year=False)}
            return machine_mapping[self.game_config["machine"]]
        rom_file = os.path.basename(self.game_config.get("main_file", ""))
        if rom_file.startswith("gnw_"):
            return _("Nintendo Game & Watch")
        return _("Arcade")

    def prelaunch(self):
        if not system.path_exists(os.path.join(self.config_dir, "mame.ini")):
            try:
                os.makedirs(self.config_dir)
            except OSError:
                pass
            system.execute(
                [self.get_executable(), "-createconfig", "-inipath", self.config_dir],
                env=runtime.get_env(),
                cwd=self.working_dir
            )
        return True

    def get_shader_params(self, shader_dir, shaders):
        """Returns a list of CLI parameters to apply a list of shaders"""
        params = []
        shader_path = os.path.join(self.working_dir, "shaders", shader_dir)
        for index, shader in enumerate(shaders):
            params += [
                "-gl_glsl",
                "-glsl_shader_mame%s" % index,
                os.path.join(shader_path, shader)
            ]
        return params

    def play(self):
        command = [self.get_executable(), "-skip_gameinfo", "-inipath", self.config_dir]
        if self.runner_config.get("video"):
            command += ["-video", self.runner_config["video"]]
        if not self.runner_config.get("fullscreen"):
            command.append("-window")
        if self.runner_config.get("waitvsync"):
            command.append("-waitvsync")
        if self.runner_config.get("uimodekey"):
            command += ["-uimodekey", self.runner_config["uimodekey"]]

        if self.runner_config.get("crt"):
            command += self.get_shader_params("CRT-geom", ["Gaussx", "Gaussy", "CRT-geom-halation"])
            command += ["-nounevenstretch"]

        if self.game_config.get("machine"):
            rompath = self.runner_config.get("rompath")
            if rompath:
                command += ["-rompath", rompath]
            command.append(self.game_config["machine"])
            device = self.game_config.get("device")
            if not device:
                return {'error': "CUSTOM", "text": "No device is set for machine %s" % self.game_config["machine"]}
            rom = self.game_config.get("main_file")
            if rom:
                command += ["-" + device, rom]
        else:
            rompath = os.path.dirname(self.game_config.get("main_file"))
            if not rompath:
                rompath = self.runner_config.get("rompath")
            rom = os.path.basename(self.game_config.get("main_file"))
            if not rompath:
                return {'error': 'PATH_NOT_SET', 'path': 'rompath'}
            command += ["-rompath", rompath, rom]

        if self.game_config.get("autoboot_command"):
            command += ["-autoboot_command", self.game_config["autoboot_command"] + "\\n"]
            if self.game_config.get("autoboot_delay"):
                command += ["-autoboot_delay", str(self.game_config["autoboot_delay"])]

        for arg in split_arguments(self.game_config.get("args")):
            command.append(arg)

        return {"command": command}
cache_dir
config_dir
default_path property readonly

Return the default path, use the runner's rompath

description
game_options
human_name
platforms property readonly

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

runnable_alone
runner_executable
runner_options
working_dir property readonly

Return the working directory to use when running the game.

xml_path
get_platform(self)
Source code in lutris/runners/mame.py
def get_platform(self):
    selected_platform = self.game_config.get("platform")
    if selected_platform:
        return self.platforms[int(selected_platform)]
    if self.game_config.get("machine"):
        machine_mapping = {choice[1]: choice[0] for choice in get_system_choices(include_year=False)}
        return machine_mapping[self.game_config["machine"]]
    rom_file = os.path.basename(self.game_config.get("main_file", ""))
    if rom_file.startswith("gnw_"):
        return _("Nintendo Game & Watch")
    return _("Arcade")
get_shader_params(self, shader_dir, shaders)

Returns a list of CLI parameters to apply a list of shaders

Source code in lutris/runners/mame.py
def get_shader_params(self, shader_dir, shaders):
    """Returns a list of CLI parameters to apply a list of shaders"""
    params = []
    shader_path = os.path.join(self.working_dir, "shaders", shader_dir)
    for index, shader in enumerate(shaders):
        params += [
            "-gl_glsl",
            "-glsl_shader_mame%s" % index,
            os.path.join(shader_path, shader)
        ]
    return params
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/mame.py
def install(self, version=None, downloader=None, callback=None):

    def on_runner_installed(*args):
        AsyncCall(write_mame_xml, notify_mame_xml)

    super().install(version=version, downloader=downloader, callback=on_runner_installed)
play(self)
Source code in lutris/runners/mame.py
def play(self):
    command = [self.get_executable(), "-skip_gameinfo", "-inipath", self.config_dir]
    if self.runner_config.get("video"):
        command += ["-video", self.runner_config["video"]]
    if not self.runner_config.get("fullscreen"):
        command.append("-window")
    if self.runner_config.get("waitvsync"):
        command.append("-waitvsync")
    if self.runner_config.get("uimodekey"):
        command += ["-uimodekey", self.runner_config["uimodekey"]]

    if self.runner_config.get("crt"):
        command += self.get_shader_params("CRT-geom", ["Gaussx", "Gaussy", "CRT-geom-halation"])
        command += ["-nounevenstretch"]

    if self.game_config.get("machine"):
        rompath = self.runner_config.get("rompath")
        if rompath:
            command += ["-rompath", rompath]
        command.append(self.game_config["machine"])
        device = self.game_config.get("device")
        if not device:
            return {'error': "CUSTOM", "text": "No device is set for machine %s" % self.game_config["machine"]}
        rom = self.game_config.get("main_file")
        if rom:
            command += ["-" + device, rom]
    else:
        rompath = os.path.dirname(self.game_config.get("main_file"))
        if not rompath:
            rompath = self.runner_config.get("rompath")
        rom = os.path.basename(self.game_config.get("main_file"))
        if not rompath:
            return {'error': 'PATH_NOT_SET', 'path': 'rompath'}
        command += ["-rompath", rompath, rom]

    if self.game_config.get("autoboot_command"):
        command += ["-autoboot_command", self.game_config["autoboot_command"] + "\\n"]
        if self.game_config.get("autoboot_delay"):
            command += ["-autoboot_delay", str(self.game_config["autoboot_delay"])]

    for arg in split_arguments(self.game_config.get("args")):
        command.append(arg)

    return {"command": command}
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/mame.py
def prelaunch(self):
    if not system.path_exists(os.path.join(self.config_dir, "mame.ini")):
        try:
            os.makedirs(self.config_dir)
        except OSError:
            pass
        system.execute(
            [self.get_executable(), "-createconfig", "-inipath", self.config_dir],
            env=runtime.get_env(),
            cwd=self.working_dir
        )
    return True
write_xml_list(self)

Write the full game list in XML to disk

Source code in lutris/runners/mame.py
def write_xml_list(self):
    """Write the full game list in XML to disk"""
    os.makedirs(self.cache_dir, exist_ok=True)
    output = system.execute(
        [self.get_executable(), "-listxml"],
        env=runtime.get_env()
    )
    if output:
        with open(self.xml_path, "w", encoding='utf-8') as xml_file:
            xml_file.write(output)
        logger.info("MAME XML list written to %s", self.xml_path)
    else:
        logger.warning("Couldn't get any output for mame -listxml")

get_system_choices(include_year=True)

Return list of systems for inclusion in dropdown

Source code in lutris/runners/mame.py
def get_system_choices(include_year=True):
    """Return list of systems for inclusion in dropdown"""
    if not system.path_exists(MAME_XML_PATH, exclude_empty=True):
        mame_inst = mame()
        if mame_inst.is_installed():
            AsyncCall(write_mame_xml, notify_mame_xml)
        return []
    for system_id, info in sorted(
        get_supported_systems(MAME_XML_PATH).items(),
        key=lambda sys: (sys[1]["manufacturer"], sys[1]["description"]),
    ):
        if info["description"].startswith(info["manufacturer"]):
            template = ""
        else:
            template = "%(manufacturer)s "
        template += "%(description)s"
        if include_year:
            template += " %(year)s"
        system_name = template % info
        system_name = system_name.replace("<generic>", "").strip()
        yield (system_name, system_id)

notify_mame_xml(result, error)

Source code in lutris/runners/mame.py
def notify_mame_xml(result, error):
    if error:
        logger.error("Failed writing MAME XML")
    elif result:
        logger.info("Finished writing MAME XML")

write_mame_xml(force=False)

Source code in lutris/runners/mame.py
def write_mame_xml(force=False):
    if not system.path_exists(MAME_CACHE_DIR):
        system.create_folder(MAME_CACHE_DIR)
    if system.path_exists(MAME_XML_PATH, exclude_empty=True) and not force:
        return False
    logger.info("Writing full game list from MAME to %s", MAME_XML_PATH)
    mame_inst = mame()
    mame_inst.write_xml_list()
    if system.get_disk_size(MAME_XML_PATH) == 0:
        logger.warning("MAME did not write anything to %s", MAME_XML_PATH)
        return False
    return True

mednafen

DEFAULT_MEDNAFEN_SCALER

mednafen (Runner)

Source code in lutris/runners/mednafen.py
class mednafen(Runner):
    human_name = _("Mednafen")
    description = _("Multi-system emulator: NES, PC Engine, PSX…")
    platforms = [
        _("Nintendo Game Boy (Color)"),
        _("Nintendo Game Boy Advance"),
        _("Sega Game Gear"),
        _("Sega Genesis/Mega Drive"),
        _("Atari Lynx"),
        _("Sega Master System"),
        _("SNK Neo Geo Pocket (Color)"),
        _("Nintendo NES"),
        _("NEC PC Engine TurboGrafx-16"),
        _("NEC PC-FX"),
        _("Sony PlayStation"),
        _("Sega Saturn"),
        _("Nintendo SNES"),
        _("Bandai WonderSwan"),
        _("Nintendo Virtual Boy"),
    ]
    machine_choices = (
        (_("Game Boy (Color)"), "gb"),
        (_("Game Boy Advance"), "gba"),
        (_("Game Gear"), "gg"),
        (_("Genesis/Mega Drive"), "md"),
        (_("Lynx"), "lynx"),
        (_("Master System"), "sms"),
        (_("Neo Geo Pocket (Color)"), "gnp"),
        (_("NES"), "nes"),
        (_("PC Engine"), "pce_fast"),
        (_("PC-FX"), "pcfx"),
        (_("PlayStation"), "psx"),
        (_("Saturn"), "ss"),
        (_("SNES"), "snes"),
        (_("WonderSwan"), "wswan"),
        (_("Virtual Boy"), "vb"),
    )
    runner_executable = "mednafen/bin/mednafen"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "help":
            _("The game data, commonly called a ROM image. \n"
              "Mednafen supports GZIP and ZIP compressed ROMs."),
        },
        {
            "option": "machine",
            "type": "choice",
            "label": _("Machine type"),
            "choices": machine_choices,
            "help": _("The emulated machine."),
        },
    ]
    runner_options = [
        {
            "option": "fs",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": False
        },
        {
            "option":
            "stretch",
            "type":
            "choice",
            "label":
            _("Aspect ratio"),
            "choices": (
                (_("Disabled"), "0"),
                (_("Stretched"), "full"),
                (_("Preserve aspect ratio"), "aspect"),
                (_("Integer scale"), "aspect_int"),
                (_("Multiple of 2 scale"), "aspect_mult2"),
            ),
            "default":
            "aspect_int",
        },
        {
            "option":
            "scaler",
            "type":
            "choice",
            "label":
            _("Video scaler"),
            "choices": (
                ("none", "none"),
                ("hq2x", "hq2x"),
                ("hq3x", "hq3x"),
                ("hq4x", "hq4x"),
                ("scale2x", "scale2x"),
                ("scale3x", "scale3x"),
                ("scale4x", "scale4x"),
                ("2xsai", "2xsai"),
                ("super2xsai", "super2xsai"),
                ("supereagle", "supereagle"),
                ("nn2x", "nn2x"),
                ("nn3x", "nn3x"),
                ("nn4x", "nn4x"),
                ("nny2x", "nny2x"),
                ("nny3x", "nny3x"),
                ("nny4x", "nny4x"),
            ),
            "default":
            DEFAULT_MEDNAFEN_SCALER,
        },
        {
            "option":
            "sound_device",
            "type":
            "choice",
            "label":
            _("Sound device"),
            "choices": (
                (_("Mednafen default"), "default"),
                (_("ALSA default"), "sexyal-literal-default"),
                ("hw:0", "hw:0,0"),
                ("hw:1", "hw:1,0"),
                ("hw:2", "hw:2,0"),
            ),
            "default":
            "sexyal-literal-default"
        },
        {
            "option": "dont_map_controllers",
            "type": "bool",
            "label": _("Use default Mednafen controller configuration"),
            "default": False,
        },
    ]

    def get_platform(self):
        machine = self.game_config.get("machine")
        if machine:
            for index, choice in enumerate(self.machine_choices):
                if choice[1] == machine:
                    return self.platforms[index]
        return ""

    def find_joysticks(self):
        """ Detect connected joysticks and return their ids """
        joy_ids = []
        if not self.is_installed:
            return []
        with subprocess.Popen(
            [self.get_executable(), "dummy"],
            stdout=subprocess.PIPE,
            universal_newlines=True,
        ) as mednafen_process:
            output = mednafen_process.communicate()[0].split("\n")
        found = False
        joy_list = []
        for line in output:
            if found and "Joystick" in line:
                joy_list.append(line)
            else:
                found = False
            if "Initializing joysticks" in line:
                found = True

        for joy in joy_list:
            index = joy.find("Unique ID:")
            joy_id = joy[index + 11:]
            logger.debug("Joystick found id %s ", joy_id)
            joy_ids.append(joy_id)
        return joy_ids

    @staticmethod
    def set_joystick_controls(joy_ids, machine):
        """ Setup joystick mappings per machine """

        # Get the controller mappings
        controller_mappings = get_controller_mappings()
        if not controller_mappings:
            logger.warning("No controller detected for joysticks %s.", joy_ids)
            return []

        # TODO currently only supports the first controller. Add support for other controllers.
        mapping = controller_mappings[0][1]

        # Construct a dictionnary of button codes to parse to mendafen
        map_code = {
            "a": "",
            "b": "",
            "c": "",
            "x": "",
            "y": "",
            "z": "",
            "back": "",
            "start": "",
            "leftshoulder": "",
            "rightshoulder": "",
            "lefttrigger": "",
            "righttrigger": "",
            "leftstick": "",
            "rightstick": "",
            "select": "",
            "shoulder_l": "",
            "shoulder_r": "",
            "i": "",
            "ii": "",
            "iii": "",
            "iv": "",
            "v": "",
            "vi": "",
            "run": "",
            "ls": "",
            "rs": "",
            "fire1": "",
            "fire2": "",
            "option_1": "",
            "option_2": "",
            "cross": "",
            "circle": "",
            "square": "",
            "triangle": "",
            "r1": "",
            "r2": "",
            "l1": "",
            "l2": "",
            "option": "",
            "l": "",
            "r": "",
            "right-x": "",
            "right-y": "",
            "left-x": "",
            "left-y": "",
            "up-x": "",
            "up-y": "",
            "down-x": "",
            "down-y": "",
            "up-l": "",
            "up-r": "",
            "down-l": "",
            "down-r": "",
            "left-l": "",
            "left-r": "",
            "right-l": "",
            "right-r": "",
            "lstick_up": "0000c001",
            "lstick_down": "00008001",
            "lstick_right": "00008000",
            "lstick_left": "0000c000",
            "rstick_up": "0000c003",
            "rstick_down": "00008003",
            "rstick_left": "0000c002",
            "rstick_right": "00008002",
            "dpup": "0000c005",
            "dpdown": "00008005",
            "dpleft": "0000c004",
            "dpright": "00008004",
        }

        # Insert the button mapping number into the map_codes
        for button in mapping.keys:
            bttn_id = mapping.keys[button]
            if bttn_id[0] == "b":  # it's a button
                map_code[button] = "000000" + bttn_id[1:].zfill(2)

        # Duplicate button names that are emulated in mednanfen
        map_code["up"] = map_code["dpup"]  #
        map_code["down"] = map_code["dpdown"]  #
        map_code["left"] = map_code["dpleft"]  # Multiple systems
        map_code["right"] = map_code["dpright"]
        map_code["select"] = map_code["back"]  #
        map_code["shoulder_r"] = map_code["rightshoulder"]  # GBA
        map_code["shoulder_l"] = map_code["leftshoulder"]  #
        map_code["i"] = map_code["b"]  #
        map_code["ii"] = map_code["a"]  #
        map_code["iii"] = map_code["leftshoulder"]
        map_code["iv"] = map_code["y"]  # PCEngine and PCFX
        map_code["v"] = map_code["x"]  #
        map_code["vi"] = map_code["rightshoulder"]
        map_code["run"] = map_code["start"]  #
        map_code["ls"] = map_code["leftshoulder"]  #
        map_code["rs"] = map_code["rightshoulder"]  # Saturn
        map_code["c"] = map_code["righttrigger"]  #
        map_code["z"] = map_code["lefttrigger"]  #
        map_code["fire1"] = map_code["a"]  # Master System
        map_code["fire2"] = map_code["b"]  #
        map_code["option_1"] = map_code["x"]  # Lynx
        map_code["option_2"] = map_code["y"]  #
        map_code["r1"] = map_code["rightshoulder"]  #
        map_code["r2"] = map_code["righttrigger"]  #
        map_code["l1"] = map_code["leftshoulder"]  #
        map_code["l2"] = map_code["lefttrigger"]  # PlayStation
        map_code["cross"] = map_code["a"]  #
        map_code["circle"] = map_code["b"]  #
        map_code["square"] = map_code["x"]  #
        map_code["triangle"] = map_code["y"]  #
        map_code["option"] = map_code["select"]  # NeoGeo pocket
        map_code["l"] = map_code["leftshoulder"]  # SNES
        map_code["r"] = map_code["rightshoulder"]  #
        map_code["right-x"] = map_code["dpright"]  #
        map_code["left-x"] = map_code["dpleft"]  #
        map_code["up-x"] = map_code["dpup"]  #
        map_code["down-x"] = map_code["dpdown"]  # Wonder Swan
        map_code["right-y"] = map_code["lstick_right"]
        map_code["left-y"] = map_code["lstick_left"]  #
        map_code["up-y"] = map_code["lstick_up"]  #
        map_code["down-y"] = map_code["lstick_down"]  #
        map_code["up-l"] = map_code["dpup"]  #
        map_code["down-l"] = map_code["dpdown"]  #
        map_code["left-l"] = map_code["dpleft"]  #
        map_code["right-l"] = map_code["dpright"]  #
        map_code["up-r"] = map_code["rstick_up"]  #
        map_code["down-r"] = map_code["rstick_down"]  # Virtual boy
        map_code["left-r"] = map_code["rstick_left"]  #
        map_code["right-r"] = map_code["rstick_right"]  #
        map_code["lt"] = map_code["leftshoulder"]  #
        map_code["rt"] = map_code["rightshoulder"]  #

        # Define which buttons to use for each machine
        layout = {
            "nes": ["a", "b", "start", "select", "up", "down", "left", "right"],
            "gb": ["a", "b", "start", "select", "up", "down", "left", "right"],
            "gba": [
                "a",
                "b",
                "shoulder_r",
                "shoulder_l",
                "start",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "pce": [
                "i",
                "ii",
                "iii",
                "iv",
                "v",
                "vi",
                "run",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "ss": [
                "a",
                "b",
                "c",
                "x",
                "y",
                "z",
                "ls",
                "rs",
                "start",
                "up",
                "down",
                "left",
                "right",
            ],
            "gg": ["button1", "button2", "start", "up", "down", "left", "right"],
            "md": [
                "a",
                "b",
                "c",
                "x",
                "y",
                "z",
                "start",
                "up",
                "down",
                "left",
                "right",
            ],
            "sms": ["fire1", "fire2", "up", "down", "left", "right"],
            "lynx": ["a", "b", "option_1", "option_2", "up", "down", "left", "right"],
            "psx": [
                "cross",
                "circle",
                "square",
                "triangle",
                "l1",
                "l2",
                "r1",
                "r2",
                "start",
                "select",
                "lstick_up",
                "lstick_down",
                "lstick_right",
                "lstick_left",
                "rstick_up",
                "rstick_down",
                "rstick_left",
                "rstick_right",
                "up",
                "down",
                "left",
                "right",
            ],
            "pcfx": [
                "i",
                "ii",
                "iii",
                "iv",
                "v",
                "vi",
                "run",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "ngp": ["a", "b", "option", "up", "down", "left", "right"],
            "snes": [
                "a",
                "b",
                "x",
                "y",
                "l",
                "r",
                "start",
                "select",
                "up",
                "down",
                "left",
                "right",
            ],
            "wswan": [
                "a",
                "b",
                "right-x",
                "right-y",
                "left-x",
                "left-y",
                "up-x",
                "up-y",
                "down-x",
                "down-y",
                "start",
            ],
            "vb": [
                "up-l",
                "down-l",
                "left-l",
                "right-l",
                "up-r",
                "down-r",
                "left-r",
                "right-r",
                "a",
                "b",
                "lt",
                "rt",
            ],
        }
        # Select a the gamepad type
        controls = []
        if machine in ["gg", "lynx", "wswan", "gb", "gba", "vb"]:
            gamepad = "builtin.gamepad"
        elif machine in ["md"]:
            gamepad = "port1.gamepad6"
            controls.append("-md.input.port1")
            controls.append("gamepad6")
        elif machine in ["psx"]:
            gamepad = "port1.dualshock"
            controls.append("-psx.input.port1")
            controls.append("dualshock")
        else:
            gamepad = "port1.gamepad"

        # Construct the controlls options
        for button in layout[machine]:
            controls.append("-{}.input.{}.{}".format(machine, gamepad, button))
            controls.append("joystick {} {}".format(joy_ids[0], map_code[button]))
        return controls

    def play(self):
        """Runs the game"""
        rom = self.game_config.get("main_file") or ""
        machine = self.game_config.get("machine") or ""

        fullscreen = self.runner_config.get("fs") or "0"
        if fullscreen is True:
            fullscreen = "1"
        elif fullscreen is False:
            fullscreen = "0"

        stretch = self.runner_config.get("stretch") or "0"
        scaler = self.runner_config.get("scaler") or DEFAULT_MEDNAFEN_SCALER
        sound_device = self.runner_config.get("sound_device")

        xres, yres = DISPLAY_MANAGER.get_current_resolution()
        options = [
            "-fs",
            fullscreen,
            "-force_module",
            machine,
            "-sound.device",
            sound_device,
            "-" + machine + ".xres",
            xres,
            "-" + machine + ".yres",
            yres,
            "-" + machine + ".stretch",
            stretch,
            "-" + machine + ".special",
            scaler,
            "-" + machine + ".videoip",
            "1",
        ]
        joy_ids = self.find_joysticks()
        dont_map_controllers = self.runner_config.get("dont_map_controllers")
        if joy_ids and not dont_map_controllers:
            controls = self.set_joystick_controls(joy_ids, machine)
            for control in controls:
                options.append(control)

        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}

        command = [self.get_executable()]
        for option in options:
            command.append(option)
        command.append(rom)
        return {"command": command}
description
game_options
human_name
machine_choices
platforms
runner_executable
runner_options
find_joysticks(self)

Detect connected joysticks and return their ids

Source code in lutris/runners/mednafen.py
def find_joysticks(self):
    """ Detect connected joysticks and return their ids """
    joy_ids = []
    if not self.is_installed:
        return []
    with subprocess.Popen(
        [self.get_executable(), "dummy"],
        stdout=subprocess.PIPE,
        universal_newlines=True,
    ) as mednafen_process:
        output = mednafen_process.communicate()[0].split("\n")
    found = False
    joy_list = []
    for line in output:
        if found and "Joystick" in line:
            joy_list.append(line)
        else:
            found = False
        if "Initializing joysticks" in line:
            found = True

    for joy in joy_list:
        index = joy.find("Unique ID:")
        joy_id = joy[index + 11:]
        logger.debug("Joystick found id %s ", joy_id)
        joy_ids.append(joy_id)
    return joy_ids
get_platform(self)
Source code in lutris/runners/mednafen.py
def get_platform(self):
    machine = self.game_config.get("machine")
    if machine:
        for index, choice in enumerate(self.machine_choices):
            if choice[1] == machine:
                return self.platforms[index]
    return ""
play(self)

Runs the game

Source code in lutris/runners/mednafen.py
def play(self):
    """Runs the game"""
    rom = self.game_config.get("main_file") or ""
    machine = self.game_config.get("machine") or ""

    fullscreen = self.runner_config.get("fs") or "0"
    if fullscreen is True:
        fullscreen = "1"
    elif fullscreen is False:
        fullscreen = "0"

    stretch = self.runner_config.get("stretch") or "0"
    scaler = self.runner_config.get("scaler") or DEFAULT_MEDNAFEN_SCALER
    sound_device = self.runner_config.get("sound_device")

    xres, yres = DISPLAY_MANAGER.get_current_resolution()
    options = [
        "-fs",
        fullscreen,
        "-force_module",
        machine,
        "-sound.device",
        sound_device,
        "-" + machine + ".xres",
        xres,
        "-" + machine + ".yres",
        yres,
        "-" + machine + ".stretch",
        stretch,
        "-" + machine + ".special",
        scaler,
        "-" + machine + ".videoip",
        "1",
    ]
    joy_ids = self.find_joysticks()
    dont_map_controllers = self.runner_config.get("dont_map_controllers")
    if joy_ids and not dont_map_controllers:
        controls = self.set_joystick_controls(joy_ids, machine)
        for control in controls:
            options.append(control)

    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}

    command = [self.get_executable()]
    for option in options:
        command.append(option)
    command.append(rom)
    return {"command": command}
set_joystick_controls(joy_ids, machine) staticmethod

Setup joystick mappings per machine

Source code in lutris/runners/mednafen.py
@staticmethod
def set_joystick_controls(joy_ids, machine):
    """ Setup joystick mappings per machine """

    # Get the controller mappings
    controller_mappings = get_controller_mappings()
    if not controller_mappings:
        logger.warning("No controller detected for joysticks %s.", joy_ids)
        return []

    # TODO currently only supports the first controller. Add support for other controllers.
    mapping = controller_mappings[0][1]

    # Construct a dictionnary of button codes to parse to mendafen
    map_code = {
        "a": "",
        "b": "",
        "c": "",
        "x": "",
        "y": "",
        "z": "",
        "back": "",
        "start": "",
        "leftshoulder": "",
        "rightshoulder": "",
        "lefttrigger": "",
        "righttrigger": "",
        "leftstick": "",
        "rightstick": "",
        "select": "",
        "shoulder_l": "",
        "shoulder_r": "",
        "i": "",
        "ii": "",
        "iii": "",
        "iv": "",
        "v": "",
        "vi": "",
        "run": "",
        "ls": "",
        "rs": "",
        "fire1": "",
        "fire2": "",
        "option_1": "",
        "option_2": "",
        "cross": "",
        "circle": "",
        "square": "",
        "triangle": "",
        "r1": "",
        "r2": "",
        "l1": "",
        "l2": "",
        "option": "",
        "l": "",
        "r": "",
        "right-x": "",
        "right-y": "",
        "left-x": "",
        "left-y": "",
        "up-x": "",
        "up-y": "",
        "down-x": "",
        "down-y": "",
        "up-l": "",
        "up-r": "",
        "down-l": "",
        "down-r": "",
        "left-l": "",
        "left-r": "",
        "right-l": "",
        "right-r": "",
        "lstick_up": "0000c001",
        "lstick_down": "00008001",
        "lstick_right": "00008000",
        "lstick_left": "0000c000",
        "rstick_up": "0000c003",
        "rstick_down": "00008003",
        "rstick_left": "0000c002",
        "rstick_right": "00008002",
        "dpup": "0000c005",
        "dpdown": "00008005",
        "dpleft": "0000c004",
        "dpright": "00008004",
    }

    # Insert the button mapping number into the map_codes
    for button in mapping.keys:
        bttn_id = mapping.keys[button]
        if bttn_id[0] == "b":  # it's a button
            map_code[button] = "000000" + bttn_id[1:].zfill(2)

    # Duplicate button names that are emulated in mednanfen
    map_code["up"] = map_code["dpup"]  #
    map_code["down"] = map_code["dpdown"]  #
    map_code["left"] = map_code["dpleft"]  # Multiple systems
    map_code["right"] = map_code["dpright"]
    map_code["select"] = map_code["back"]  #
    map_code["shoulder_r"] = map_code["rightshoulder"]  # GBA
    map_code["shoulder_l"] = map_code["leftshoulder"]  #
    map_code["i"] = map_code["b"]  #
    map_code["ii"] = map_code["a"]  #
    map_code["iii"] = map_code["leftshoulder"]
    map_code["iv"] = map_code["y"]  # PCEngine and PCFX
    map_code["v"] = map_code["x"]  #
    map_code["vi"] = map_code["rightshoulder"]
    map_code["run"] = map_code["start"]  #
    map_code["ls"] = map_code["leftshoulder"]  #
    map_code["rs"] = map_code["rightshoulder"]  # Saturn
    map_code["c"] = map_code["righttrigger"]  #
    map_code["z"] = map_code["lefttrigger"]  #
    map_code["fire1"] = map_code["a"]  # Master System
    map_code["fire2"] = map_code["b"]  #
    map_code["option_1"] = map_code["x"]  # Lynx
    map_code["option_2"] = map_code["y"]  #
    map_code["r1"] = map_code["rightshoulder"]  #
    map_code["r2"] = map_code["righttrigger"]  #
    map_code["l1"] = map_code["leftshoulder"]  #
    map_code["l2"] = map_code["lefttrigger"]  # PlayStation
    map_code["cross"] = map_code["a"]  #
    map_code["circle"] = map_code["b"]  #
    map_code["square"] = map_code["x"]  #
    map_code["triangle"] = map_code["y"]  #
    map_code["option"] = map_code["select"]  # NeoGeo pocket
    map_code["l"] = map_code["leftshoulder"]  # SNES
    map_code["r"] = map_code["rightshoulder"]  #
    map_code["right-x"] = map_code["dpright"]  #
    map_code["left-x"] = map_code["dpleft"]  #
    map_code["up-x"] = map_code["dpup"]  #
    map_code["down-x"] = map_code["dpdown"]  # Wonder Swan
    map_code["right-y"] = map_code["lstick_right"]
    map_code["left-y"] = map_code["lstick_left"]  #
    map_code["up-y"] = map_code["lstick_up"]  #
    map_code["down-y"] = map_code["lstick_down"]  #
    map_code["up-l"] = map_code["dpup"]  #
    map_code["down-l"] = map_code["dpdown"]  #
    map_code["left-l"] = map_code["dpleft"]  #
    map_code["right-l"] = map_code["dpright"]  #
    map_code["up-r"] = map_code["rstick_up"]  #
    map_code["down-r"] = map_code["rstick_down"]  # Virtual boy
    map_code["left-r"] = map_code["rstick_left"]  #
    map_code["right-r"] = map_code["rstick_right"]  #
    map_code["lt"] = map_code["leftshoulder"]  #
    map_code["rt"] = map_code["rightshoulder"]  #

    # Define which buttons to use for each machine
    layout = {
        "nes": ["a", "b", "start", "select", "up", "down", "left", "right"],
        "gb": ["a", "b", "start", "select", "up", "down", "left", "right"],
        "gba": [
            "a",
            "b",
            "shoulder_r",
            "shoulder_l",
            "start",
            "select",
            "up",
            "down",
            "left",
            "right",
        ],
        "pce": [
            "i",
            "ii",
            "iii",
            "iv",
            "v",
            "vi",
            "run",
            "select",
            "up",
            "down",
            "left",
            "right",
        ],
        "ss": [
            "a",
            "b",
            "c",
            "x",
            "y",
            "z",
            "ls",
            "rs",
            "start",
            "up",
            "down",
            "left",
            "right",
        ],
        "gg": ["button1", "button2", "start", "up", "down", "left", "right"],
        "md": [
            "a",
            "b",
            "c",
            "x",
            "y",
            "z",
            "start",
            "up",
            "down",
            "left",
            "right",
        ],
        "sms": ["fire1", "fire2", "up", "down", "left", "right"],
        "lynx": ["a", "b", "option_1", "option_2", "up", "down", "left", "right"],
        "psx": [
            "cross",
            "circle",
            "square",
            "triangle",
            "l1",
            "l2",
            "r1",
            "r2",
            "start",
            "select",
            "lstick_up",
            "lstick_down",
            "lstick_right",
            "lstick_left",
            "rstick_up",
            "rstick_down",
            "rstick_left",
            "rstick_right",
            "up",
            "down",
            "left",
            "right",
        ],
        "pcfx": [
            "i",
            "ii",
            "iii",
            "iv",
            "v",
            "vi",
            "run",
            "select",
            "up",
            "down",
            "left",
            "right",
        ],
        "ngp": ["a", "b", "option", "up", "down", "left", "right"],
        "snes": [
            "a",
            "b",
            "x",
            "y",
            "l",
            "r",
            "start",
            "select",
            "up",
            "down",
            "left",
            "right",
        ],
        "wswan": [
            "a",
            "b",
            "right-x",
            "right-y",
            "left-x",
            "left-y",
            "up-x",
            "up-y",
            "down-x",
            "down-y",
            "start",
        ],
        "vb": [
            "up-l",
            "down-l",
            "left-l",
            "right-l",
            "up-r",
            "down-r",
            "left-r",
            "right-r",
            "a",
            "b",
            "lt",
            "rt",
        ],
    }
    # Select a the gamepad type
    controls = []
    if machine in ["gg", "lynx", "wswan", "gb", "gba", "vb"]:
        gamepad = "builtin.gamepad"
    elif machine in ["md"]:
        gamepad = "port1.gamepad6"
        controls.append("-md.input.port1")
        controls.append("gamepad6")
    elif machine in ["psx"]:
        gamepad = "port1.dualshock"
        controls.append("-psx.input.port1")
        controls.append("dualshock")
    else:
        gamepad = "port1.gamepad"

    # Construct the controlls options
    for button in layout[machine]:
        controls.append("-{}.input.{}.{}".format(machine, gamepad, button))
        controls.append("joystick {} {}".format(joy_ids[0], map_code[button]))
    return controls

mupen64plus

mupen64plus (Runner)

Source code in lutris/runners/mupen64plus.py
class mupen64plus(Runner):
    human_name = _("Mupen64Plus")
    description = _("Nintendo 64 emulator")
    platforms = [_("Nintendo 64")]
    runner_executable = "mupen64plus/mupen64plus"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "help": _("The game data, commonly called a ROM image."),
        }
    ]
    runner_options = [
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": True,
        },
        {
            "option": "hideosd",
            "type": "bool",
            "label": _("Hide OSD"),
            "default": True
        },
    ]

    @property
    def working_dir(self):
        return os.path.join(settings.RUNNER_DIR, "mupen64plus")

    def play(self):
        arguments = [self.get_executable()]
        if self.runner_config.get("hideosd"):
            arguments.append("--noosd")
        else:
            arguments.append("--osd")
        if self.runner_config.get("fullscreen"):
            arguments.append("--fullscreen")
        else:
            arguments.append("--windowed")
        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        arguments.append(rom)
        return {"command": arguments}
description
game_options
human_name
platforms
runner_executable
runner_options
working_dir property readonly

Return the working directory to use when running the game.

play(self)
Source code in lutris/runners/mupen64plus.py
def play(self):
    arguments = [self.get_executable()]
    if self.runner_config.get("hideosd"):
        arguments.append("--noosd")
    else:
        arguments.append("--osd")
    if self.runner_config.get("fullscreen"):
        arguments.append("--fullscreen")
    else:
        arguments.append("--windowed")
    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    arguments.append(rom)
    return {"command": arguments}

o2em

o2em (Runner)

Source code in lutris/runners/o2em.py
class o2em(Runner):
    human_name = _("O2EM")
    description = _("Magnavox Odyssey² Emulator")
    platforms = (
        _("Magnavox Odyssey²"),
        _("Phillips C52"),
        _("Phillips Videopac+"),
        _("Brandt Jopac"),
    )
    bios_path = os.path.expanduser("~/.o2em/bios")
    runner_executable = "o2em/o2em"

    checksums = {
        "o2rom": "562d5ebf9e030a40d6fabfc2f33139fd",
        "c52": "f1071cdb0b6b10dde94d3bc8a6146387",
        "jopac": "279008e4a0db2dc5f1c048853b033828",
        "g7400": "79008e4a0db2dc5f1c048853b033828",
    }

    bios_choices = [
        (_("Magnavox Odyssey²"), "o2rom"),
        (_("Phillips C52"), "c52"),
        (_("Phillips Videopac+"), "g7400"),
        (_("Brandt Jopac"), "jopac"),
    ]
    controller_choices = [
        (_("Disable"), "0"),
        (_("Arrow Keys and Right Shift"), "1"),
        (_("W,S,A,D,SPACE"), "2"),
        (_("Joystick"), "3"),
    ]
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "default_path": "game_path",
            "help": _("The game data, commonly called a ROM image."),
        }
    ]
    runner_options = [
        {
            "option": "bios",
            "type": "choice",
            "choices": bios_choices,
            "label": _("BIOS"),
            "default": "o2rom",
        },
        {
            "option": "controller1",
            "type": "choice",
            "choices": controller_choices,
            "label": _("First controller"),
            "default": "2",
        },
        {
            "option": "controller2",
            "type": "choice",
            "choices": controller_choices,
            "label": _("Second controller"),
            "default": "1",
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": False,
        },
        {
            "option": "scanlines",
            "type": "bool",
            "label": _("Scanlines display style"),
            "default": False,
            "help": _("Activates a display filter adding scanlines to imitate "
                      "the displays of yesteryear."),
        },
    ]

    def get_platform(self):
        bios = self.runner_config.get("bios")
        if bios:
            for i, b in enumerate(self.bios_choices):
                if b[1] == bios:
                    return self.platforms[i]
        return ""

    def install(self, version=None, downloader=None, callback=None):

        def on_runner_installed(*args):
            if not system.path_exists(self.bios_path):
                os.makedirs(self.bios_path)
            if callback:
                callback()

        super().install(version, downloader, on_runner_installed)

    def play(self):
        arguments = ["-biosdir=%s" % self.bios_path]

        if self.runner_config.get("fullscreen"):
            arguments.append("-fullscreen")

        if self.runner_config.get("scanlines"):
            arguments.append("-scanlines")

        if "controller1" in self.runner_config:
            arguments.append("-s1=%s" % self.runner_config["controller1"])
        if "controller2" in self.runner_config:
            arguments.append("-s2=%s" % self.runner_config["controller2"])
        rom_path = self.game_config.get("main_file") or ""
        if not system.path_exists(rom_path):
            return {"error": "FILE_NOT_FOUND", "file": rom_path}
        romdir = os.path.dirname(rom_path)
        romfile = os.path.basename(rom_path)
        arguments.append("-romdir=%s/" % romdir)
        arguments.append(romfile)
        return {"command": [self.get_executable()] + arguments}
bios_choices
bios_path
checksums
controller_choices
description
game_options
human_name
platforms
runner_executable
runner_options
get_platform(self)
Source code in lutris/runners/o2em.py
def get_platform(self):
    bios = self.runner_config.get("bios")
    if bios:
        for i, b in enumerate(self.bios_choices):
            if b[1] == bios:
                return self.platforms[i]
    return ""
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/o2em.py
def install(self, version=None, downloader=None, callback=None):

    def on_runner_installed(*args):
        if not system.path_exists(self.bios_path):
            os.makedirs(self.bios_path)
        if callback:
            callback()

    super().install(version, downloader, on_runner_installed)
play(self)
Source code in lutris/runners/o2em.py
def play(self):
    arguments = ["-biosdir=%s" % self.bios_path]

    if self.runner_config.get("fullscreen"):
        arguments.append("-fullscreen")

    if self.runner_config.get("scanlines"):
        arguments.append("-scanlines")

    if "controller1" in self.runner_config:
        arguments.append("-s1=%s" % self.runner_config["controller1"])
    if "controller2" in self.runner_config:
        arguments.append("-s2=%s" % self.runner_config["controller2"])
    rom_path = self.game_config.get("main_file") or ""
    if not system.path_exists(rom_path):
        return {"error": "FILE_NOT_FOUND", "file": rom_path}
    romdir = os.path.dirname(rom_path)
    romfile = os.path.basename(rom_path)
    arguments.append("-romdir=%s/" % romdir)
    arguments.append(romfile)
    return {"command": [self.get_executable()] + arguments}

openmsx

openmsx (Runner)

Source code in lutris/runners/openmsx.py
class openmsx(Runner):
    human_name = _("openMSX")
    description = _("MSX computer emulator")
    platforms = [_("MSX, MSX2, MSX2+, MSX turboR")]
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "help": _("The game data, commonly called a ROM image."),
        }
    ]

    def play(self):
        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        return {"command": [self.get_executable(), rom]}
description
game_options
human_name
platforms
play(self)
Source code in lutris/runners/openmsx.py
def play(self):
    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    return {"command": [self.get_executable(), rom]}

osmose

osmose (Runner)

Source code in lutris/runners/osmose.py
class osmose(Runner):
    human_name = _("Osmose")
    description = _("Sega Master System Emulator")
    platforms = [_("Sega Master System")]
    runner_executable = "osmose/osmose"
    game_options = [
        {
            "option":
            "main_file",
            "type":
            "file",
            "label":
            _("ROM file"),
            "default_path":
            "game_path",
            "help": _(
                "The game data, commonly called a ROM image.\n"
                "Supported formats: SMS and GG files. ZIP compressed "
                "ROMs are supported."
            ),
        }
    ]
    runner_options = [{
        "option": "fullscreen",
        "type": "bool",
        "label": _("Fullscreen"),
        "default": False,
    }]

    def play(self):
        """Run Sega Master System game"""
        arguments = [self.get_executable()]
        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        arguments.append(rom)
        if self.runner_config.get("fullscreen"):
            arguments.append("-fs")
            arguments.append("-bilinear")
        return {"command": arguments}
description
game_options
human_name
platforms
runner_executable
runner_options
play(self)

Run Sega Master System game

Source code in lutris/runners/osmose.py
def play(self):
    """Run Sega Master System game"""
    arguments = [self.get_executable()]
    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    arguments.append(rom)
    if self.runner_config.get("fullscreen"):
        arguments.append("-fs")
        arguments.append("-bilinear")
    return {"command": arguments}

pcsx2

pcsx2 (Runner)

Source code in lutris/runners/pcsx2.py
class pcsx2(Runner):
    human_name = _("PCSX2")
    description = _("PlayStation 2 emulator")
    platforms = [_("Sony PlayStation 2")]
    runnable_alone = True
    runner_executable = "pcsx2/PCSX2"
    arch = "i386"
    require_libs = ["libOpenGL.so.0", "libgdk-x11-2.0.so.0", "libEGL.so.1"]
    game_options = [{
        "option": "main_file",
        "type": "file",
        "label": _("ISO file"),
        "default_path": "game_path",
    }]

    runner_options = [
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": False,
        },
        {
            "option": "full_boot",
            "type": "bool",
            "label": _("Fullboot"),
            "default": False
        },
        {
            "option": "nogui",
            "type": "bool",
            "label": _("No GUI"),
            "default": False
        },
        {
            "option": "config_file",
            "type": "file",
            "label": _("Custom config file"),
            "advanced": True,
        },
        {
            "option": "config_path",
            "type": "directory_chooser",
            "label": _("Custom config path"),
            "advanced": True,
        },
    ]

    def play(self):
        arguments = [self.get_executable()]

        if self.runner_config.get("fullscreen"):
            arguments.append("--fullscreen")
        if self.runner_config.get("full_boot"):
            arguments.append("--fullboot")
        if self.runner_config.get("nogui"):
            arguments.append("--nogui")
        if self.runner_config.get("config_file"):
            arguments.append("--cfg={}".format(self.runner_config["config_file"]))
        if self.runner_config.get("config_path"):
            arguments.append("--cfgpath={}".format(self.runner_config["config_path"]))

        iso = self.game_config.get("main_file") or ""
        if not system.path_exists(iso):
            return {"error": "FILE_NOT_FOUND", "file": iso}
        arguments.append(iso)
        return {"command": arguments}
arch
description
game_options
human_name
platforms
require_libs
runnable_alone
runner_executable
runner_options
play(self)
Source code in lutris/runners/pcsx2.py
def play(self):
    arguments = [self.get_executable()]

    if self.runner_config.get("fullscreen"):
        arguments.append("--fullscreen")
    if self.runner_config.get("full_boot"):
        arguments.append("--fullboot")
    if self.runner_config.get("nogui"):
        arguments.append("--nogui")
    if self.runner_config.get("config_file"):
        arguments.append("--cfg={}".format(self.runner_config["config_file"]))
    if self.runner_config.get("config_path"):
        arguments.append("--cfgpath={}".format(self.runner_config["config_path"]))

    iso = self.game_config.get("main_file") or ""
    if not system.path_exists(iso):
        return {"error": "FILE_NOT_FOUND", "file": iso}
    arguments.append(iso)
    return {"command": arguments}

pico8

Runner for the PICO-8 fantasy console

DOWNLOAD_URL

pico8 (Runner)

Source code in lutris/runners/pico8.py
class pico8(Runner):
    description = _("Runs PICO-8 fantasy console cartridges")
    multiple_versions = False
    human_name = _("PICO-8")
    platforms = [_("PICO-8")]
    game_options = [
        {
            "option": "main_file",
            "type": "string",
            "label": _("Cartridge file/URL/ID"),
            "help": _("You can put a .p8.png file path, URL, or BBS cartridge ID here."),
        }
    ]

    runner_options = [
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": True,
            "help": _("Launch in fullscreen."),
        },
        {
            "option": "window_size",
            "label": _("Window size"),
            "type": "string",
            "default": "640x512",
            "help": _("The initial size of the game window."),
        },
        {
            "option": "splore",
            "type": "bool",
            "label": _("Start in splore mode"),
            "default": False,
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Extra arguments"),
            "default": "",
            "help": _("Extra arguments to the executable"),
            "advanced": True,
        },
        {
            "option": "engine",
            "type": "string",
            "label": _("Engine (web only)"),
            "default": "pico8_0111g_4",
            "help": _("Name of engine (will be downloaded) or local file path"),
        },
    ]

    system_options_override = [{"option": "disable_runtime", "default": True}]

    runner_executable = "pico8/web.py"

    def __init__(self, config=None):
        super().__init__(config)

        self.runnable_alone = self.is_native

    def __repr__(self):
        return _("PICO-8 runner (%s)") % self.config

    def install(self, version=None, downloader=None, callback=None):
        opts = {}
        if callback:
            opts["callback"] = callback
        opts["dest"] = settings.RUNNER_DIR + "/pico8"
        opts["merge_single"] = True
        if downloader:
            opts["downloader"] = downloader
        else:
            raise RuntimeError("Unsupported download for this runner")
        self.download_and_extract(DOWNLOAD_URL, **opts)

    @property
    def is_native(self):
        return self.runner_config.get("runner_executable", "") != ""

    @property
    def engine_path(self):
        engine = self.runner_config.get("engine")
        if not engine.lower().endswith(".js") and not os.path.exists(engine):
            engine = os.path.join(
                settings.RUNNER_DIR,
                "pico8/web/engines",
                self.runner_config.get("engine") + ".js",
            )
        return engine

    @property
    def cart_path(self):
        main_file = self.game_config.get("main_file")
        if self.is_native and main_file.startswith("http"):
            return os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png")
        if not os.path.exists(main_file) and main_file.isdigit():
            return os.path.join(settings.RUNNER_DIR, "pico8/cartridges", main_file + ".p8.png")
        return main_file

    @property
    def launch_args(self):
        if self.is_native:
            args = [self.get_executable()]
            args.append("-windowed")
            args.append("0" if self.runner_config.get("fullscreen") else "1")
            if self.runner_config.get("splore"):
                args.append("-splore")

            size = self.runner_config.get("window_size").split("x")
            if len(size) == 2:
                args.append("-width")
                args.append(size[0])
                args.append("-height")
                args.append(size[1])
            extra_args = self.runner_config.get("args", "")
            for arg in split_arguments(extra_args):
                args.append(arg)
        else:
            args = [
                self.get_executable(),
                os.path.join(settings.RUNNER_DIR, "pico8/web/player.html"),
                "--window-size",
                self.runner_config.get("window_size"),
            ]
        return args

    def get_run_data(self):
        return {"command": self.launch_args, "env": self.get_env(os_env=False)}

    def is_installed(self, version=None, fallback=True, min_version=None):
        """Checks if pico8 runner is installed and if the pico8 executable available.
        """
        if self.is_native and system.path_exists(self.runner_config.get("runner_executable")):
            return True
        return system.path_exists(os.path.join(settings.RUNNER_DIR, "pico8/web/player.html"))

    def prelaunch(self):
        if not self.game_config.get("main_file") and self.is_installed():
            return True
        if os.path.exists(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png")):
            os.remove(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png"))

        # Don't download cartridge if using web backend and cart is url
        if self.is_native or not self.game_config.get("main_file").startswith("http"):
            if not os.path.exists(self.game_config.get("main_file")) and (
                self.game_config.get("main_file").isdigit() or self.game_config.get("main_file").startswith("http")
            ):
                if not self.game_config.get("main_file").startswith("http"):
                    pid = int(self.game_config.get("main_file"))
                    num = math.floor(pid / 10000)
                    downloadUrl = ("https://www.lexaloffle.com/bbs/cposts/" + str(num) + "/" + str(pid) + ".p8.png")
                else:
                    downloadUrl = self.game_config.get("main_file")
                cartPath = self.cart_path
                system.create_folder(os.path.dirname(cartPath))

                downloadCompleted = False

                def on_downloaded_cart():
                    nonlocal downloadCompleted
                    # If we are offline we don't want an empty file to overwrite the cartridge
                    if dl.downloaded_size:
                        shutil.move(cartPath + ".download", cartPath)
                    else:
                        os.remove(cartPath + ".download")
                    downloadCompleted = True

                dl = Downloader(
                    downloadUrl,
                    cartPath + ".download",
                    True,
                    callback=on_downloaded_cart,
                )
                dl.start()

                # Wait for download to complete or continue if it exists (to work in offline mode)
                while not os.path.exists(cartPath):
                    if downloadCompleted or dl.state == Downloader.ERROR:
                        logger.error("Could not download cartridge from %s", downloadUrl)
                        return False
                    sleep(0.1)

        # Download js engine
        if not self.is_native and not os.path.exists(self.runner_config.get("engine")):
            enginePath = os.path.join(
                settings.RUNNER_DIR,
                "pico8/web/engines",
                self.runner_config.get("engine") + ".js",
            )
            if not os.path.exists(enginePath):
                downloadUrl = ("https://www.lexaloffle.com/bbs/" + self.runner_config.get("engine") + ".js")
                system.create_folder(os.path.dirname(enginePath))
                downloadCompleted = False

                def on_downloaded_engine():
                    nonlocal downloadCompleted
                    downloadCompleted = True

                dl = Downloader(downloadUrl, enginePath, True, callback=on_downloaded_engine)
                dl.start()
                dl.thread.join()  # Doesn't actually wait until finished

                # Waits for download to complete
                while not os.path.exists(enginePath):
                    if downloadCompleted or dl.state == Downloader.ERROR:
                        logger.error("Could not download engine from %s", downloadUrl)
                        return False
                    sleep(0.1)

        return True

    def play(self):
        launch_info = {}
        launch_info["env"] = self.get_env(os_env=False)

        game_data = get_game_by_field(self.config.game_config_id, "configpath")

        command = self.launch_args

        if self.is_native:
            if not self.runner_config.get("splore"):
                command.append("-run")
            cartPath = self.cart_path
            if not os.path.exists(cartPath):
                return {"error": "FILE_NOT_FOUND", "file": cartPath}
            command.append(cartPath)

        else:
            command.append("--name")
            command.append(game_data.get("name") + " - PICO-8")

            # icon = datapath.get_icon_path(game_data.get("slug"))
            # if icon:
            #     command.append("--icon")
            #     command.append(icon)

            webargs = {
                "cartridge": self.cart_path,
                "engine": self.engine_path,
                "fullscreen": self.runner_config.get("fullscreen") is True,
            }
            command.append("--execjs")
            command.append("load_config(" + json.dumps(webargs) + ")")

        launch_info["command"] = command
        return launch_info
cart_path property readonly
description
engine_path property readonly
game_options
human_name
is_native property readonly
launch_args property readonly
multiple_versions
platforms
runner_executable
runner_options
system_options_override
__init__(self, config=None) special
Source code in lutris/runners/pico8.py
def __init__(self, config=None):
    super().__init__(config)

    self.runnable_alone = self.is_native
__repr__(self) special
Source code in lutris/runners/pico8.py
def __repr__(self):
    return _("PICO-8 runner (%s)") % self.config
get_run_data(self)

Return dict with command (exe & args list) and env vars (dict).

Reimplement in derived runner if need be.

Source code in lutris/runners/pico8.py
def get_run_data(self):
    return {"command": self.launch_args, "env": self.get_env(os_env=False)}
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/pico8.py
def install(self, version=None, downloader=None, callback=None):
    opts = {}
    if callback:
        opts["callback"] = callback
    opts["dest"] = settings.RUNNER_DIR + "/pico8"
    opts["merge_single"] = True
    if downloader:
        opts["downloader"] = downloader
    else:
        raise RuntimeError("Unsupported download for this runner")
    self.download_and_extract(DOWNLOAD_URL, **opts)
is_installed(self, version=None, fallback=True, min_version=None)

Checks if pico8 runner is installed and if the pico8 executable available.

Source code in lutris/runners/pico8.py
def is_installed(self, version=None, fallback=True, min_version=None):
    """Checks if pico8 runner is installed and if the pico8 executable available.
    """
    if self.is_native and system.path_exists(self.runner_config.get("runner_executable")):
        return True
    return system.path_exists(os.path.join(settings.RUNNER_DIR, "pico8/web/player.html"))
play(self)
Source code in lutris/runners/pico8.py
def play(self):
    launch_info = {}
    launch_info["env"] = self.get_env(os_env=False)

    game_data = get_game_by_field(self.config.game_config_id, "configpath")

    command = self.launch_args

    if self.is_native:
        if not self.runner_config.get("splore"):
            command.append("-run")
        cartPath = self.cart_path
        if not os.path.exists(cartPath):
            return {"error": "FILE_NOT_FOUND", "file": cartPath}
        command.append(cartPath)

    else:
        command.append("--name")
        command.append(game_data.get("name") + " - PICO-8")

        # icon = datapath.get_icon_path(game_data.get("slug"))
        # if icon:
        #     command.append("--icon")
        #     command.append(icon)

        webargs = {
            "cartridge": self.cart_path,
            "engine": self.engine_path,
            "fullscreen": self.runner_config.get("fullscreen") is True,
        }
        command.append("--execjs")
        command.append("load_config(" + json.dumps(webargs) + ")")

    launch_info["command"] = command
    return launch_info
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/pico8.py
def prelaunch(self):
    if not self.game_config.get("main_file") and self.is_installed():
        return True
    if os.path.exists(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png")):
        os.remove(os.path.join(settings.RUNNER_DIR, "pico8/cartridges", "tmp.p8.png"))

    # Don't download cartridge if using web backend and cart is url
    if self.is_native or not self.game_config.get("main_file").startswith("http"):
        if not os.path.exists(self.game_config.get("main_file")) and (
            self.game_config.get("main_file").isdigit() or self.game_config.get("main_file").startswith("http")
        ):
            if not self.game_config.get("main_file").startswith("http"):
                pid = int(self.game_config.get("main_file"))
                num = math.floor(pid / 10000)
                downloadUrl = ("https://www.lexaloffle.com/bbs/cposts/" + str(num) + "/" + str(pid) + ".p8.png")
            else:
                downloadUrl = self.game_config.get("main_file")
            cartPath = self.cart_path
            system.create_folder(os.path.dirname(cartPath))

            downloadCompleted = False

            def on_downloaded_cart():
                nonlocal downloadCompleted
                # If we are offline we don't want an empty file to overwrite the cartridge
                if dl.downloaded_size:
                    shutil.move(cartPath + ".download", cartPath)
                else:
                    os.remove(cartPath + ".download")
                downloadCompleted = True

            dl = Downloader(
                downloadUrl,
                cartPath + ".download",
                True,
                callback=on_downloaded_cart,
            )
            dl.start()

            # Wait for download to complete or continue if it exists (to work in offline mode)
            while not os.path.exists(cartPath):
                if downloadCompleted or dl.state == Downloader.ERROR:
                    logger.error("Could not download cartridge from %s", downloadUrl)
                    return False
                sleep(0.1)

    # Download js engine
    if not self.is_native and not os.path.exists(self.runner_config.get("engine")):
        enginePath = os.path.join(
            settings.RUNNER_DIR,
            "pico8/web/engines",
            self.runner_config.get("engine") + ".js",
        )
        if not os.path.exists(enginePath):
            downloadUrl = ("https://www.lexaloffle.com/bbs/" + self.runner_config.get("engine") + ".js")
            system.create_folder(os.path.dirname(enginePath))
            downloadCompleted = False

            def on_downloaded_engine():
                nonlocal downloadCompleted
                downloadCompleted = True

            dl = Downloader(downloadUrl, enginePath, True, callback=on_downloaded_engine)
            dl.start()
            dl.thread.join()  # Doesn't actually wait until finished

            # Waits for download to complete
            while not os.path.exists(enginePath):
                if downloadCompleted or dl.state == Downloader.ERROR:
                    logger.error("Could not download engine from %s", downloadUrl)
                    return False
                sleep(0.1)

    return True

redream

redream (Runner)

Source code in lutris/runners/redream.py
class redream(Runner):
    human_name = _("Redream")
    description = _("Sega Dreamcast emulator")
    platforms = [_("Sega Dreamcast")]
    runner_executable = "redream/redream"
    download_url = "https://redream.io/download/redream.x86_64-linux-v1.5.0.tar.gz"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("Disc image file"),
            "help": _("Game data file\n" "Supported formats: GDI, CDI, CHD"),
        }
    ]
    runner_options = [
        {"option": "fs", "type": "bool", "label": _("Fullscreen"), "default": False},
        {
            "option": "ar",
            "type": "choice",
            "label": _("Aspect Ratio"),
            "choices": [(_("4:3"), "4:3"), (_("Stretch"), "stretch")],
            "default": "4:3",
        },
        {
            "option": "region",
            "type": "choice",
            "label": _("Region"),
            "choices": [(_("USA"), "usa"), (_("Europe"), "europe"), (_("Japan"), "japan")],
            "default": "usa",
        },
        {
            "option": "language",
            "type": "choice",
            "label": _("System Language"),
            "choices": [
                (_("English"), "english"),
                (_("German"), "german"),
                (_("French"), "french"),
                (_("Spanish"), "spanish"),
                (_("Italian"), "italian"),
                (_("Japanese"), "japanese"),
            ],
            "default": "english",
        },
        {
            "option": "broadcast",
            "type": "choice",
            "label": "Television System",
            "choices": [
                (_("NTSC"), "ntsc"),
                (_("PAL"), "pal"),
                (_("PAL-M (Brazil)"), "pal_m"),
                (_("PAL-N (Argentina, Paraguay, Uruguay)"), "pal_n"),
            ],
            "default": "ntsc",
        },
        {
            "option": "time_sync",
            "type": "choice",
            "label": _("Time Sync"),
            "choices": [
                (_("Audio and video"), "audio and video"),
                (_("Audio"), "audio"),
                (_("Video"), "video"),
                (_("None"), "none"),
            ],
            "default": "audio and video",
            "advanced": True,
        },
        {
            "option": "int_res",
            "type": "choice",
            "label": _("Internal Video Resolution Scale"),
            "choices": [
                ("×1", "1"),
                ("×2", "2"),
                ("×3", "3"),
                ("×4", "4"),
                ("×5", "5"),
                ("×6", "6"),
                ("×7", "7"),
                ("×8", "8"),
            ],
            "default": "2",
            "advanced": True,
            "help": _("Only available in premium version."),
        },
    ]

    def install(self, version=None, downloader=None, callback=None):
        def on_runner_installed(*args):
            dlg = QuestionDialog(
                {
                    "question": _("Do you want to select a premium license file?"),
                    "title": _("Use premium version?"),
                }
            )
            if dlg.result == dlg.YES:
                license_dlg = FileDialog(_("Select a license file"))
                license_filename = license_dlg.filename
                if not license_filename:
                    return
                shutil.copy(
                    license_filename, os.path.join(settings.RUNNER_DIR, "redream")
                )

        super().install(
            version=version, downloader=downloader, callback=on_runner_installed
        )

    def play(self):
        command = [self.get_executable()]

        if self.runner_config.get("fs") is True:
            command.append("--fullscreen=1")
        else:
            command.append("--fullscreen=0")

        if self.runner_config.get("ar"):
            command.append("--aspect=" + self.runner_config.get("ar"))

        if self.runner_config.get("region"):
            command.append("--region=" + self.runner_config.get("region"))

        if self.runner_config.get("language"):
            command.append("--language=" + self.runner_config.get("language"))

        if self.runner_config.get("broadcast"):
            command.append("--broadcast=" + self.runner_config.get("broadcast"))

        if self.runner_config.get("time_sync"):
            command.append("--time_sync=" + self.runner_config.get("time_sync"))

        if self.runner_config.get("int_res"):
            command.append("--res=" + self.runner_config.get("int_res"))

        command.append(self.game_config.get("main_file"))

        return {"command": command}
description
download_url
game_options
human_name
platforms
runner_executable
runner_options
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/redream.py
def install(self, version=None, downloader=None, callback=None):
    def on_runner_installed(*args):
        dlg = QuestionDialog(
            {
                "question": _("Do you want to select a premium license file?"),
                "title": _("Use premium version?"),
            }
        )
        if dlg.result == dlg.YES:
            license_dlg = FileDialog(_("Select a license file"))
            license_filename = license_dlg.filename
            if not license_filename:
                return
            shutil.copy(
                license_filename, os.path.join(settings.RUNNER_DIR, "redream")
            )

    super().install(
        version=version, downloader=downloader, callback=on_runner_installed
    )
play(self)
Source code in lutris/runners/redream.py
def play(self):
    command = [self.get_executable()]

    if self.runner_config.get("fs") is True:
        command.append("--fullscreen=1")
    else:
        command.append("--fullscreen=0")

    if self.runner_config.get("ar"):
        command.append("--aspect=" + self.runner_config.get("ar"))

    if self.runner_config.get("region"):
        command.append("--region=" + self.runner_config.get("region"))

    if self.runner_config.get("language"):
        command.append("--language=" + self.runner_config.get("language"))

    if self.runner_config.get("broadcast"):
        command.append("--broadcast=" + self.runner_config.get("broadcast"))

    if self.runner_config.get("time_sync"):
        command.append("--time_sync=" + self.runner_config.get("time_sync"))

    if self.runner_config.get("int_res"):
        command.append("--res=" + self.runner_config.get("int_res"))

    command.append(self.game_config.get("main_file"))

    return {"command": command}

reicast

reicast (Runner)

Source code in lutris/runners/reicast.py
class reicast(Runner):
    human_name = _("Reicast")
    description = _("Sega Dreamcast emulator")
    platforms = [_("Sega Dreamcast")]
    runner_executable = "reicast/reicast.elf"
    entry_point_option = "iso"

    joypads = None

    game_options = [
        {
            "option": "iso",
            "type": "file",
            "label": _("Disc image file"),
            "help": _("The game data.\n"
                      "Supported formats: ISO, CDI"),
        }
    ]

    def __init__(self, config=None):
        super().__init__(config)

        self.runner_options = [
            {
                "option": "fullscreen",
                "type": "bool",
                "label": _("Fullscreen"),
                "default": False,
            },
            {
                "option": "device_id_1",
                "type": "choice",
                "label": _("Gamepad 1"),
                "choices": self.get_joypads,
                "default": "-1",
            },
            {
                "option": "device_id_2",
                "type": "choice",
                "label": _("Gamepad 2"),
                "choices": self.get_joypads,
                "default": "-1",
            },
            {
                "option": "device_id_3",
                "type": "choice",
                "label": _("Gamepad 3"),
                "choices": self.get_joypads,
                "default": "-1",
            },
            {
                "option": "device_id_4",
                "type": "choice",
                "label": _("Gamepad 4"),
                "choices": self.get_joypads,
                "default": "-1",
            },
        ]

    def install(self, version=None, downloader=None, callback=None):

        def on_runner_installed(*args):
            mapping_path = system.create_folder("~/.reicast/mappings")
            mapping_source = os.path.join(settings.RUNNER_DIR, "reicast/mappings")
            for mapping_file in os.listdir(mapping_source):
                shutil.copy(os.path.join(mapping_source, mapping_file), mapping_path)

            system.create_folder("~/.reicast/data")
            NoticeDialog(_("You have to copy valid BIOS files to ~/.reicast/data before playing"))

        super().install(version, downloader, on_runner_installed)

    def get_joypads(self):
        """Return list of joypad in a format usable in the options"""
        if self.joypads:
            return self.joypads
        joypad_list = [("No joystick", "-1")]
        joypad_devices = joypad.get_joypads()
        name_counter = Counter([j[1] for j in joypad_devices])
        name_indexes = {}
        for (dev, joy_name) in joypad_devices:
            dev_id = re.findall(r"(\d+)", dev)[0]
            if name_counter[joy_name] > 1:
                if joy_name not in name_indexes:
                    index = 1
                else:
                    index = name_indexes[joy_name] + 1
                name_indexes[joy_name] = index
            else:
                index = 0
            if index:
                joy_name += " (%d)" % index
            joypad_list.append((joy_name, dev_id))
        self.joypads = joypad_list
        return joypad_list

    @staticmethod
    def write_config(config):
        # use RawConfigParser to preserve case-sensitive configs written by Reicast
        # otherwise, Reicast will write with mixed-case and Lutris will overwrite with all lowercase
        #   which will confuse Reicast
        parser = RawConfigParser()
        parser.optionxform = lambda option: option

        config_path = os.path.expanduser("~/.reicast/emu.cfg")
        if system.path_exists(config_path):
            with open(config_path, "r", encoding='utf-8') as config_file:
                parser.read_file(config_file)

        for section in config:
            if not parser.has_section(section):
                parser.add_section(section)
            for (key, value) in config[section].items():
                parser.set(section, key, str(value))

        with open(config_path, "w", encoding='utf-8') as config_file:
            parser.write(config_file)

    def play(self):
        fullscreen = "1" if self.runner_config.get("fullscreen") else "0"
        reicast_config = {
            "x11": {
                "fullscreen": fullscreen
            },
            "input": {},
            "players": {
                "nb": "1"
            },
        }
        players = 1
        reicast_config["input"] = {}
        for index in range(1, 5):
            config_string = "device_id_%d" % index
            joy_id = self.runner_config.get(config_string) or "-1"
            reicast_config["input"]["evdev_{}".format(config_string)] = joy_id
            if index > 1 and joy_id != "-1":
                players += 1
        reicast_config["players"]["nb"] = players

        self.write_config(reicast_config)

        iso = self.game_config.get("iso")
        command = [self.get_executable(), "-config", "config:image={}".format(iso)]
        return {"command": command}
description
entry_point_option
game_options
human_name
joypads
platforms
runner_executable
__init__(self, config=None) special
Source code in lutris/runners/reicast.py
def __init__(self, config=None):
    super().__init__(config)

    self.runner_options = [
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": False,
        },
        {
            "option": "device_id_1",
            "type": "choice",
            "label": _("Gamepad 1"),
            "choices": self.get_joypads,
            "default": "-1",
        },
        {
            "option": "device_id_2",
            "type": "choice",
            "label": _("Gamepad 2"),
            "choices": self.get_joypads,
            "default": "-1",
        },
        {
            "option": "device_id_3",
            "type": "choice",
            "label": _("Gamepad 3"),
            "choices": self.get_joypads,
            "default": "-1",
        },
        {
            "option": "device_id_4",
            "type": "choice",
            "label": _("Gamepad 4"),
            "choices": self.get_joypads,
            "default": "-1",
        },
    ]
get_joypads(self)

Return list of joypad in a format usable in the options

Source code in lutris/runners/reicast.py
def get_joypads(self):
    """Return list of joypad in a format usable in the options"""
    if self.joypads:
        return self.joypads
    joypad_list = [("No joystick", "-1")]
    joypad_devices = joypad.get_joypads()
    name_counter = Counter([j[1] for j in joypad_devices])
    name_indexes = {}
    for (dev, joy_name) in joypad_devices:
        dev_id = re.findall(r"(\d+)", dev)[0]
        if name_counter[joy_name] > 1:
            if joy_name not in name_indexes:
                index = 1
            else:
                index = name_indexes[joy_name] + 1
            name_indexes[joy_name] = index
        else:
            index = 0
        if index:
            joy_name += " (%d)" % index
        joypad_list.append((joy_name, dev_id))
    self.joypads = joypad_list
    return joypad_list
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/reicast.py
def install(self, version=None, downloader=None, callback=None):

    def on_runner_installed(*args):
        mapping_path = system.create_folder("~/.reicast/mappings")
        mapping_source = os.path.join(settings.RUNNER_DIR, "reicast/mappings")
        for mapping_file in os.listdir(mapping_source):
            shutil.copy(os.path.join(mapping_source, mapping_file), mapping_path)

        system.create_folder("~/.reicast/data")
        NoticeDialog(_("You have to copy valid BIOS files to ~/.reicast/data before playing"))

    super().install(version, downloader, on_runner_installed)
play(self)
Source code in lutris/runners/reicast.py
def play(self):
    fullscreen = "1" if self.runner_config.get("fullscreen") else "0"
    reicast_config = {
        "x11": {
            "fullscreen": fullscreen
        },
        "input": {},
        "players": {
            "nb": "1"
        },
    }
    players = 1
    reicast_config["input"] = {}
    for index in range(1, 5):
        config_string = "device_id_%d" % index
        joy_id = self.runner_config.get(config_string) or "-1"
        reicast_config["input"]["evdev_{}".format(config_string)] = joy_id
        if index > 1 and joy_id != "-1":
            players += 1
    reicast_config["players"]["nb"] = players

    self.write_config(reicast_config)

    iso = self.game_config.get("iso")
    command = [self.get_executable(), "-config", "config:image={}".format(iso)]
    return {"command": command}
write_config(config) staticmethod
Source code in lutris/runners/reicast.py
@staticmethod
def write_config(config):
    # use RawConfigParser to preserve case-sensitive configs written by Reicast
    # otherwise, Reicast will write with mixed-case and Lutris will overwrite with all lowercase
    #   which will confuse Reicast
    parser = RawConfigParser()
    parser.optionxform = lambda option: option

    config_path = os.path.expanduser("~/.reicast/emu.cfg")
    if system.path_exists(config_path):
        with open(config_path, "r", encoding='utf-8') as config_file:
            parser.read_file(config_file)

    for section in config:
        if not parser.has_section(section):
            parser.add_section(section)
        for (key, value) in config[section].items():
            parser.set(section, key, str(value))

    with open(config_path, "w", encoding='utf-8') as config_file:
        parser.write(config_file)

residualvm

ResidualVM runner

RESIDUALVM_CONFIG_FILE

residualvm (Runner)

Source code in lutris/runners/residualvm.py
class residualvm(Runner):
    human_name = _("ResidualVM")
    platforms = [_("Linux")]  # TODO
    description = _("3D point-and-click adventure games engine")
    runner_executable = "residualvm/residualvm"
    game_options = [
        {
            "option": "game_id",
            "type": "string",
            "label": _("Game identifier")
        },
        {
            "option": "path",
            "type": "directory_chooser",
            "label": _("Game files location")
        },
        {
            "option": "subtitles",
            "label": _("Enable subtitles (if the game has voice)"),
            "type": "bool",
            "default": False,
        },
    ]
    runner_options = [
        {
            "option": "fullscreen",
            "label": _("Fullscreen"),
            "type": "bool",
            "default": False,
        },
        {
            "option": "renderer",
            "label": _("Renderer"),
            "type": "choice",
            "choices": (
                ("OpenGL", "opengl"),
                (_("OpenGL shaders"), "opengl_shaders"),
                (_("Software"), "software"),
            ),
            "default": "opengl",
        },
        {
            "option": "show-fps",
            "label": _("Display FPS information"),
            "type": "bool",
            "default": False,
        },
    ]

    @property
    def game_path(self):
        return self.game_config.get("path")

    def get_residualvm_data_dir(self):
        root_dir = os.path.dirname(self.get_executable())
        return os.path.join(root_dir, "data")

    def play(self):
        command = [
            self.get_executable(),
            "--extrapath=%s" % self.get_residualvm_data_dir(),
            "--themepath=%s" % self.get_residualvm_data_dir(),
        ]

        # Options

        if self.game_config.get("subtitles"):
            command.append("--subtitles")

        if self.runner_config.get("fullscreen"):
            command.append("--fullscreen")
        else:
            command.append("--no-fullscreen")

        renderer = self.runner_config.get("renderer")
        if renderer:
            command.append("--renderer=%s" % renderer)

        if self.runner_config.get("show-fps"):
            command.append("--show-fps")
        else:
            command.append("--no-show-fps")
        # /Options

        command.append("--path=%s" % self.game_path)
        command.append(self.game_config.get("game_id"))

        launch_info = {"command": command}

        return launch_info

    def get_game_list(self):
        """Return the entire list of games supported by ResidualVM."""
        with subprocess.Popen([self.get_executable(), "--list-games"],
                              stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as residualvm_process:
            residual_output = residualvm_process.communicate()[0]
            game_list = str.split(residual_output, "\n")
        game_array = []
        game_list_start = False
        for game in game_list:
            if game_list_start:
                if len(game) > 1:
                    dir_limit = game.index(" ")
                else:
                    dir_limit = None
                if dir_limit is not None:
                    game_dir = game[0:dir_limit]
                    game_name = game[dir_limit + 1:len(game)].strip()
                    game_array.append([game_dir, game_name])
            # The actual list is below a separator
            if game.startswith("-----"):
                game_list_start = True
        return game_array
description
game_options
game_path property readonly

Return the directory where the game is installed.

human_name
platforms
runner_executable
runner_options
get_game_list(self)

Return the entire list of games supported by ResidualVM.

Source code in lutris/runners/residualvm.py
def get_game_list(self):
    """Return the entire list of games supported by ResidualVM."""
    with subprocess.Popen([self.get_executable(), "--list-games"],
                          stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as residualvm_process:
        residual_output = residualvm_process.communicate()[0]
        game_list = str.split(residual_output, "\n")
    game_array = []
    game_list_start = False
    for game in game_list:
        if game_list_start:
            if len(game) > 1:
                dir_limit = game.index(" ")
            else:
                dir_limit = None
            if dir_limit is not None:
                game_dir = game[0:dir_limit]
                game_name = game[dir_limit + 1:len(game)].strip()
                game_array.append([game_dir, game_name])
        # The actual list is below a separator
        if game.startswith("-----"):
            game_list_start = True
    return game_array
get_residualvm_data_dir(self)
Source code in lutris/runners/residualvm.py
def get_residualvm_data_dir(self):
    root_dir = os.path.dirname(self.get_executable())
    return os.path.join(root_dir, "data")
play(self)
Source code in lutris/runners/residualvm.py
def play(self):
    command = [
        self.get_executable(),
        "--extrapath=%s" % self.get_residualvm_data_dir(),
        "--themepath=%s" % self.get_residualvm_data_dir(),
    ]

    # Options

    if self.game_config.get("subtitles"):
        command.append("--subtitles")

    if self.runner_config.get("fullscreen"):
        command.append("--fullscreen")
    else:
        command.append("--no-fullscreen")

    renderer = self.runner_config.get("renderer")
    if renderer:
        command.append("--renderer=%s" % renderer)

    if self.runner_config.get("show-fps"):
        command.append("--show-fps")
    else:
        command.append("--no-show-fps")
    # /Options

    command.append("--path=%s" % self.game_path)
    command.append(self.game_config.get("game_id"))

    launch_info = {"command": command}

    return launch_info

rpcs3

rpcs3 (Runner)

Source code in lutris/runners/rpcs3.py
class rpcs3(Runner):
    human_name = _("RPCS3")
    description = _("PlayStation 3 emulator")
    platforms = [_("Sony PlayStation 3")]
    runnable_alone = True
    runner_executable = "rpcs3/rpcs3"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "default_path": "game_path",
            "label": _("Path to EBOOT.BIN"),
        }
    ]
    runner_options = [{"option": "nogui", "type": "bool", "label": _("No GUI"), "default": False}]

    # RPCS3 currently uses an AppImage, no need for the runtime.
    system_options_override = [{"option": "disable_runtime", "default": True}]

    def play(self):
        arguments = [self.get_executable()]

        if self.runner_config.get("nogui"):
            arguments.append("--no-gui")

        eboot = self.game_config.get("main_file") or ""
        if not system.path_exists(eboot):
            return {"error": "FILE_NOT_FOUND", "file": eboot}
        arguments.append(eboot)
        return {"command": arguments}
description
game_options
human_name
platforms
runnable_alone
runner_executable
runner_options
system_options_override
play(self)
Source code in lutris/runners/rpcs3.py
def play(self):
    arguments = [self.get_executable()]

    if self.runner_config.get("nogui"):
        arguments.append("--no-gui")

    eboot = self.game_config.get("main_file") or ""
    if not system.path_exists(eboot):
        return {"error": "FILE_NOT_FOUND", "file": eboot}
    arguments.append(eboot)
    return {"command": arguments}

runner

Base module for runners

Runner

Generic runner (base class for other runners).

Source code in lutris/runners/runner.py
class Runner:  # pylint: disable=too-many-public-methods

    """Generic runner (base class for other runners)."""

    multiple_versions = False
    platforms = []
    runnable_alone = False
    game_options = []
    runner_options = []
    system_options_override = []
    context_menu_entries = []
    require_libs = []
    runner_executable = None
    entry_point_option = "main_file"
    download_url = None
    arch = None  # If the runner is only available for an architecture that isn't x86_64

    def __init__(self, config=None):
        """Initialize runner."""
        self.config = config
        if config:
            self.game_data = get_game_by_field(self.config.game_config_id, "configpath")
        else:
            self.game_data = {}

    def __lt__(self, other):
        return self.name < other.name

    @property
    def description(self):
        """Return the class' docstring as the description."""
        return self.__doc__

    @description.setter
    def description(self, value):
        """Leave the ability to override the docstring."""
        self.__doc__ = value  # What the shit

    @property
    def name(self):
        return self.__class__.__name__

    @property
    def default_config(self):
        return LutrisConfig(runner_slug=self.name)

    @property
    def game_config(self):
        """Return the cascaded game config as a dict."""
        return self.config.game_config if self.config else {}

    @property
    def runner_config(self):
        """Return the cascaded runner config as a dict."""
        if self.config:
            return self.config.runner_config
        return self.default_config.runner_config

    @property
    def system_config(self):
        """Return the cascaded system config as a dict."""
        if self.config:
            return self.config.system_config
        return self.default_config.system_config

    @property
    def default_path(self):
        """Return the default path where games are installed."""
        return self.system_config.get("game_path")

    @property
    def game_path(self):
        """Return the directory where the game is installed."""
        game_path = self.game_data.get("directory")
        if game_path:
            return game_path

        # Default to the directory where the entry point is located.
        entry_point = self.game_config.get(self.entry_point_option)
        if entry_point:
            return os.path.dirname(os.path.expanduser(entry_point))
        return ""

    @property
    def library_folders(self):
        """Return a list of paths where a game might be installed"""
        return []

    @property
    def working_dir(self):
        """Return the working directory to use when running the game."""
        return self.game_path or os.path.expanduser("~/")

    @property
    def shader_cache_dir(self):
        """Return the cache directory for this runner to use. We create
        this if it does not exist."""
        path = os.path.join(settings.SHADER_CACHE_DIR, self.name)
        if not os.path.isdir(path):
            os.mkdir(path)
        return path

    @property
    def nvidia_shader_cache_path(self):
        """The path to place in __GL_SHADER_DISK_CACHE_PATH; NVidia
        will place its cache cache in a subdirectory here."""
        return self.shader_cache_dir

    @property
    def discord_client_id(self):
        if self.game_data.get("discord_client_id"):
            return self.game_data.get("discord_client_id")

    def get_platform(self):
        return self.platforms[0]

    def get_runner_options(self):
        runner_options = self.runner_options[:]
        if self.runner_executable:
            runner_options.append(
                {
                    "option": "runner_executable",
                    "type": "file",
                    "label": _("Custom executable for the runner"),
                    "advanced": True,
                }
            )
        return runner_options

    def get_executable(self):
        if "runner_executable" in self.runner_config:
            runner_executable = self.runner_config["runner_executable"]
            if os.path.isfile(runner_executable):
                return runner_executable
        if not self.runner_executable:
            raise ValueError("runner_executable not set for {}".format(self.name))
        return os.path.join(settings.RUNNER_DIR, self.runner_executable)

    def get_env(self, os_env=False):
        """Return environment variables used for a game."""
        env = {}
        if os_env:
            env.update(os.environ.copy())

        # By default we'll set NVidia's shader disk cache to be
        # per-game, so it overflows less readily.
        env["__GL_SHADER_DISK_CACHE"] = "1"
        env["__GL_SHADER_DISK_CACHE_PATH"] = self.nvidia_shader_cache_path

        # Override SDL2 controller configuration
        sdl_gamecontrollerconfig = self.system_config.get("sdl_gamecontrollerconfig")
        if sdl_gamecontrollerconfig:
            path = os.path.expanduser(sdl_gamecontrollerconfig)
            if system.path_exists(path):
                with open(path, "r", encoding='utf-8') as controllerdb_file:
                    sdl_gamecontrollerconfig = controllerdb_file.read()
            env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig

        # Set monitor to use for SDL 1 games
        sdl_video_fullscreen = self.system_config.get("sdl_video_fullscreen")
        if sdl_video_fullscreen and sdl_video_fullscreen != "off":
            env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen

        # DRI Prime
        if self.system_config.get("dri_prime"):
            env["DRI_PRIME"] = "1"

        # Prime vars
        prime = self.system_config.get("prime")
        if prime:
            env["__NV_PRIME_RENDER_OFFLOAD"] = "1"
            env["__GLX_VENDOR_LIBRARY_NAME"] = "nvidia"
            env["__VK_LAYER_NV_optimus"] = "NVIDIA_only"

        # Set PulseAudio latency to 60ms
        if self.system_config.get("pulse_latency"):
            env["PULSE_LATENCY_MSEC"] = "60"

        # Vulkan ICD files
        vk_icd = self.system_config.get("vk_icd")
        if vk_icd:
            env["VK_ICD_FILENAMES"] = vk_icd

        runtime_ld_library_path = None

        if self.use_runtime():
            runtime_env = self.get_runtime_env()
            runtime_ld_library_path = runtime_env.get("LD_LIBRARY_PATH")

        if runtime_ld_library_path:
            ld_library_path = env.get("LD_LIBRARY_PATH")
            env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
                runtime_ld_library_path, ld_library_path]))

        # Apply user overrides at the end
        env.update(self.system_config.get("env") or {})

        return env

    def get_runtime_env(self):
        """Return runtime environment variables.

        This method may be overridden in runner classes.
        (Notably for Lutris wine builds)

        Returns:
            dict

        """
        return runtime.get_env(prefer_system_libs=self.system_config.get("prefer_system_libs", True))

    def prelaunch(self):
        """Run actions before running the game, override this method in runners"""
        available_libs = set()
        for lib in set(self.require_libs):
            if lib in LINUX_SYSTEM.shared_libraries:
                if self.arch:
                    if self.arch in [_lib.arch for _lib in LINUX_SYSTEM.shared_libraries[lib]]:
                        available_libs.add(lib)
                else:
                    available_libs.add(lib)
        unavailable_libs = set(self.require_libs) - available_libs
        if unavailable_libs:
            raise UnavailableLibraries(unavailable_libs, self.arch)
        return True

    def get_run_data(self):
        """Return dict with command (exe & args list) and env vars (dict).

        Reimplement in derived runner if need be."""
        return {"command": [self.get_executable()], "env": self.get_env()}

    def run(self, *args):
        """Run the runner alone."""
        if not self.runnable_alone:
            return
        if not self.is_installed():
            if not self.install_dialog():
                logger.info("Runner install cancelled")
                return

        command_data = self.get_run_data()
        command = command_data.get("command")
        env = (command_data.get("env") or {}).copy()

        if hasattr(self, "prelaunch"):
            self.prelaunch()

        command_runner = MonitoredCommand(command, runner=self, env=env)
        command_runner.start()

    def use_runtime(self):
        if runtime.RUNTIME_DISABLED:
            logger.info("Runtime disabled by environment")
            return False
        if self.system_config.get("disable_runtime"):
            logger.info("Runtime disabled by system configuration")
            return False
        return True

    def install_dialog(self):
        """Ask the user if they want to install the runner.

        Return success of runner installation.
        """
        dialog = dialogs.QuestionDialog(
            {
                "question": _("The required runner is not installed.\n"
                              "Do you wish to install it now?"),
                "title": _("Required runner unavailable"),
            }
        )
        if Gtk.ResponseType.YES == dialog.result:

            from lutris.gui.dialogs import ErrorDialog
            from lutris.gui.dialogs.download import simple_downloader
            try:
                if hasattr(self, "get_version"):
                    version = self.get_version(use_default=False)  # pylint: disable=no-member
                    self.install(downloader=simple_downloader, version=version)
                else:
                    self.install(downloader=simple_downloader)
            except RunnerInstallationError as ex:
                ErrorDialog(ex.message)

            return self.is_installed()
        return False

    def is_installed(self):
        """Return whether the runner is installed"""
        return system.path_exists(self.get_executable())

    def get_runner_version(self, version=None):
        """Get the appropriate version for a runner

        Params:
            version (str): Optional version to lookup, will return this one if found

        Returns:
            dict: Dict containing version, architecture and url for the runner, None
            if the data can't be retrieved.
        """
        logger.info(
            "Getting runner information for %s%s",
            self.name,
            " (version: %s)" % version if version else "",
        )

        try:
            request = Request("{}/api/runners/{}".format(settings.SITE_URL, self.name))
            runner_info = request.get().json

            if not runner_info:
                logger.error("Failed to get runner information")
        except HTTPError as ex:
            logger.error("Unable to get runner information: %s", ex)
            runner_info = None

        if not runner_info:
            return

        versions = runner_info.get("versions") or []
        arch = LINUX_SYSTEM.arch
        if version:
            if version.endswith("-i386") or version.endswith("-x86_64"):
                version, arch = version.rsplit("-", 1)
            versions = [v for v in versions if v["version"] == version]
        versions_for_arch = [v for v in versions if v["architecture"] == arch]
        if len(versions_for_arch) == 1:
            return versions_for_arch[0]

        if len(versions_for_arch) > 1:
            default_version = [v for v in versions_for_arch if v["default"] is True]
            if default_version:
                return default_version[0]
        elif len(versions) == 1 and LINUX_SYSTEM.is_64_bit:
            return versions[0]
        elif len(versions) > 1 and LINUX_SYSTEM.is_64_bit:
            default_version = [v for v in versions if v["default"] is True]
            if default_version:
                return default_version[0]
        # If we didn't find a proper version yet, return the first available.
        if len(versions_for_arch) >= 1:
            return versions_for_arch[0]

    def install(self, version=None, downloader=None, callback=None):
        """Install runner using package management systems."""
        logger.debug(
            "Installing %s (version=%s, downloader=%s, callback=%s)",
            self.name,
            version,
            downloader,
            callback,
        )
        opts = {"downloader": downloader, "callback": callback}
        if self.download_url:
            opts["dest"] = os.path.join(settings.RUNNER_DIR, self.name)
            return self.download_and_extract(self.download_url, **opts)
        runner = self.get_runner_version(version)
        if not runner:
            raise RunnerInstallationError("Failed to retrieve {} ({}) information".format(self.name, version))
        if not downloader:
            raise RuntimeError("Missing mandatory downloader for runner %s" % self)

        if "wine" in self.name:
            opts["merge_single"] = True
            opts["dest"] = os.path.join(
                settings.RUNNER_DIR, self.name, "{}-{}".format(runner["version"], runner["architecture"])
            )

        if self.name == "libretro" and version:
            opts["merge_single"] = False
            opts["dest"] = os.path.join(settings.RUNNER_DIR, "retroarch/cores")
        self.download_and_extract(runner["url"], **opts)

    def download_and_extract(self, url, dest=None, **opts):
        downloader = opts["downloader"]
        merge_single = opts.get("merge_single", False)
        callback = opts.get("callback")
        tarball_filename = os.path.basename(url)
        runner_archive = os.path.join(settings.CACHE_DIR, tarball_filename)
        if not dest:
            dest = settings.RUNNER_DIR
        downloader(
            url, runner_archive, self.extract, {
                "archive": runner_archive,
                "dest": dest,
                "merge_single": merge_single,
                "callback": callback,
            }
        )

    def extract(self, archive=None, dest=None, merge_single=None, callback=None):
        if not system.path_exists(archive):
            raise RunnerInstallationError("Failed to extract {}".format(archive))
        try:
            extract_archive(archive, dest, merge_single=merge_single)
        except ExtractFailure as ex:
            logger.error("Failed to extract the archive %s file may be corrupt", archive)
            raise RunnerInstallationError("Failed to extract {}: {}".format(archive, ex)) from ex
        os.remove(archive)

        if self.name == "wine":
            logger.debug("Clearing wine version cache")
            from lutris.util.wine.wine import get_wine_versions
            get_wine_versions.cache_clear()

        if self.runner_executable:
            runner_executable = os.path.join(settings.RUNNER_DIR, self.runner_executable)
            if os.path.isfile(runner_executable):
                system.make_executable(runner_executable)

        if callback:
            callback()

    @staticmethod
    def remove_game_data(app_id=None, game_path=None):
        system.remove_folder(game_path)

    def can_uninstall(self):
        runner_path = os.path.join(settings.RUNNER_DIR, self.name)
        return os.path.isdir(runner_path)

    def uninstall(self):
        runner_path = os.path.join(settings.RUNNER_DIR, self.name)
        if os.path.isdir(runner_path):
            system.remove_folder(runner_path)

    def find_option(self, options_group, option_name):
        """Retrieve an option dict if it exists in the group"""
        if options_group not in ['game_options', 'runner_options']:
            return None
        output = None
        for item in getattr(self, options_group):
            if item["option"] == option_name:
                output = item
                break
        return output

    def force_stop_game(self, game):
        """Stop the running game. If this leaves any game processes running,
        the caller will SIGKILL them (after a delay)."""
        game.kill_processes(signal.SIGTERM)
arch
context_menu_entries
default_config property readonly
default_path property readonly

Return the default path where games are installed.

description property writable

Return the class' docstring as the description.

discord_client_id property readonly
download_url
entry_point_option
game_config property readonly

Return the cascaded game config as a dict.

game_options
game_path property readonly

Return the directory where the game is installed.

library_folders property readonly

Return a list of paths where a game might be installed

multiple_versions
name property readonly
nvidia_shader_cache_path property readonly

The path to place in __GL_SHADER_DISK_CACHE_PATH; NVidia will place its cache cache in a subdirectory here.

platforms
require_libs
runnable_alone
runner_config property readonly

Return the cascaded runner config as a dict.

runner_executable
runner_options
shader_cache_dir property readonly

Return the cache directory for this runner to use. We create this if it does not exist.

system_config property readonly

Return the cascaded system config as a dict.

system_options_override
working_dir property readonly

Return the working directory to use when running the game.

__init__(self, config=None) special

Initialize runner.

Source code in lutris/runners/runner.py
def __init__(self, config=None):
    """Initialize runner."""
    self.config = config
    if config:
        self.game_data = get_game_by_field(self.config.game_config_id, "configpath")
    else:
        self.game_data = {}
__lt__(self, other) special
Source code in lutris/runners/runner.py
def __lt__(self, other):
    return self.name < other.name
can_uninstall(self)
Source code in lutris/runners/runner.py
def can_uninstall(self):
    runner_path = os.path.join(settings.RUNNER_DIR, self.name)
    return os.path.isdir(runner_path)
download_and_extract(self, url, dest=None, **opts)
Source code in lutris/runners/runner.py
def download_and_extract(self, url, dest=None, **opts):
    downloader = opts["downloader"]
    merge_single = opts.get("merge_single", False)
    callback = opts.get("callback")
    tarball_filename = os.path.basename(url)
    runner_archive = os.path.join(settings.CACHE_DIR, tarball_filename)
    if not dest:
        dest = settings.RUNNER_DIR
    downloader(
        url, runner_archive, self.extract, {
            "archive": runner_archive,
            "dest": dest,
            "merge_single": merge_single,
            "callback": callback,
        }
    )
extract(self, archive=None, dest=None, merge_single=None, callback=None)
Source code in lutris/runners/runner.py
def extract(self, archive=None, dest=None, merge_single=None, callback=None):
    if not system.path_exists(archive):
        raise RunnerInstallationError("Failed to extract {}".format(archive))
    try:
        extract_archive(archive, dest, merge_single=merge_single)
    except ExtractFailure as ex:
        logger.error("Failed to extract the archive %s file may be corrupt", archive)
        raise RunnerInstallationError("Failed to extract {}: {}".format(archive, ex)) from ex
    os.remove(archive)

    if self.name == "wine":
        logger.debug("Clearing wine version cache")
        from lutris.util.wine.wine import get_wine_versions
        get_wine_versions.cache_clear()

    if self.runner_executable:
        runner_executable = os.path.join(settings.RUNNER_DIR, self.runner_executable)
        if os.path.isfile(runner_executable):
            system.make_executable(runner_executable)

    if callback:
        callback()
find_option(self, options_group, option_name)

Retrieve an option dict if it exists in the group

Source code in lutris/runners/runner.py
def find_option(self, options_group, option_name):
    """Retrieve an option dict if it exists in the group"""
    if options_group not in ['game_options', 'runner_options']:
        return None
    output = None
    for item in getattr(self, options_group):
        if item["option"] == option_name:
            output = item
            break
    return output
force_stop_game(self, game)

Stop the running game. If this leaves any game processes running, the caller will SIGKILL them (after a delay).

Source code in lutris/runners/runner.py
def force_stop_game(self, game):
    """Stop the running game. If this leaves any game processes running,
    the caller will SIGKILL them (after a delay)."""
    game.kill_processes(signal.SIGTERM)
get_env(self, os_env=False)

Return environment variables used for a game.

Source code in lutris/runners/runner.py
def get_env(self, os_env=False):
    """Return environment variables used for a game."""
    env = {}
    if os_env:
        env.update(os.environ.copy())

    # By default we'll set NVidia's shader disk cache to be
    # per-game, so it overflows less readily.
    env["__GL_SHADER_DISK_CACHE"] = "1"
    env["__GL_SHADER_DISK_CACHE_PATH"] = self.nvidia_shader_cache_path

    # Override SDL2 controller configuration
    sdl_gamecontrollerconfig = self.system_config.get("sdl_gamecontrollerconfig")
    if sdl_gamecontrollerconfig:
        path = os.path.expanduser(sdl_gamecontrollerconfig)
        if system.path_exists(path):
            with open(path, "r", encoding='utf-8') as controllerdb_file:
                sdl_gamecontrollerconfig = controllerdb_file.read()
        env["SDL_GAMECONTROLLERCONFIG"] = sdl_gamecontrollerconfig

    # Set monitor to use for SDL 1 games
    sdl_video_fullscreen = self.system_config.get("sdl_video_fullscreen")
    if sdl_video_fullscreen and sdl_video_fullscreen != "off":
        env["SDL_VIDEO_FULLSCREEN_DISPLAY"] = sdl_video_fullscreen

    # DRI Prime
    if self.system_config.get("dri_prime"):
        env["DRI_PRIME"] = "1"

    # Prime vars
    prime = self.system_config.get("prime")
    if prime:
        env["__NV_PRIME_RENDER_OFFLOAD"] = "1"
        env["__GLX_VENDOR_LIBRARY_NAME"] = "nvidia"
        env["__VK_LAYER_NV_optimus"] = "NVIDIA_only"

    # Set PulseAudio latency to 60ms
    if self.system_config.get("pulse_latency"):
        env["PULSE_LATENCY_MSEC"] = "60"

    # Vulkan ICD files
    vk_icd = self.system_config.get("vk_icd")
    if vk_icd:
        env["VK_ICD_FILENAMES"] = vk_icd

    runtime_ld_library_path = None

    if self.use_runtime():
        runtime_env = self.get_runtime_env()
        runtime_ld_library_path = runtime_env.get("LD_LIBRARY_PATH")

    if runtime_ld_library_path:
        ld_library_path = env.get("LD_LIBRARY_PATH")
        env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
            runtime_ld_library_path, ld_library_path]))

    # Apply user overrides at the end
    env.update(self.system_config.get("env") or {})

    return env
get_executable(self)
Source code in lutris/runners/runner.py
def get_executable(self):
    if "runner_executable" in self.runner_config:
        runner_executable = self.runner_config["runner_executable"]
        if os.path.isfile(runner_executable):
            return runner_executable
    if not self.runner_executable:
        raise ValueError("runner_executable not set for {}".format(self.name))
    return os.path.join(settings.RUNNER_DIR, self.runner_executable)
get_platform(self)
Source code in lutris/runners/runner.py
def get_platform(self):
    return self.platforms[0]
get_run_data(self)

Return dict with command (exe & args list) and env vars (dict).

Reimplement in derived runner if need be.

Source code in lutris/runners/runner.py
def get_run_data(self):
    """Return dict with command (exe & args list) and env vars (dict).

    Reimplement in derived runner if need be."""
    return {"command": [self.get_executable()], "env": self.get_env()}
get_runner_options(self)
Source code in lutris/runners/runner.py
def get_runner_options(self):
    runner_options = self.runner_options[:]
    if self.runner_executable:
        runner_options.append(
            {
                "option": "runner_executable",
                "type": "file",
                "label": _("Custom executable for the runner"),
                "advanced": True,
            }
        )
    return runner_options
get_runner_version(self, version=None)

Get the appropriate version for a runner

Parameters:

Name Type Description Default
version str

Optional version to lookup, will return this one if found

None

Returns:

Type Description
dict

Dict containing version, architecture and url for the runner, None if the data can't be retrieved.

Source code in lutris/runners/runner.py
def get_runner_version(self, version=None):
    """Get the appropriate version for a runner

    Params:
        version (str): Optional version to lookup, will return this one if found

    Returns:
        dict: Dict containing version, architecture and url for the runner, None
        if the data can't be retrieved.
    """
    logger.info(
        "Getting runner information for %s%s",
        self.name,
        " (version: %s)" % version if version else "",
    )

    try:
        request = Request("{}/api/runners/{}".format(settings.SITE_URL, self.name))
        runner_info = request.get().json

        if not runner_info:
            logger.error("Failed to get runner information")
    except HTTPError as ex:
        logger.error("Unable to get runner information: %s", ex)
        runner_info = None

    if not runner_info:
        return

    versions = runner_info.get("versions") or []
    arch = LINUX_SYSTEM.arch
    if version:
        if version.endswith("-i386") or version.endswith("-x86_64"):
            version, arch = version.rsplit("-", 1)
        versions = [v for v in versions if v["version"] == version]
    versions_for_arch = [v for v in versions if v["architecture"] == arch]
    if len(versions_for_arch) == 1:
        return versions_for_arch[0]

    if len(versions_for_arch) > 1:
        default_version = [v for v in versions_for_arch if v["default"] is True]
        if default_version:
            return default_version[0]
    elif len(versions) == 1 and LINUX_SYSTEM.is_64_bit:
        return versions[0]
    elif len(versions) > 1 and LINUX_SYSTEM.is_64_bit:
        default_version = [v for v in versions if v["default"] is True]
        if default_version:
            return default_version[0]
    # If we didn't find a proper version yet, return the first available.
    if len(versions_for_arch) >= 1:
        return versions_for_arch[0]
get_runtime_env(self)

Return runtime environment variables.

This method may be overridden in runner classes. (Notably for Lutris wine builds)

Returns:

Type Description

dict

Source code in lutris/runners/runner.py
def get_runtime_env(self):
    """Return runtime environment variables.

    This method may be overridden in runner classes.
    (Notably for Lutris wine builds)

    Returns:
        dict

    """
    return runtime.get_env(prefer_system_libs=self.system_config.get("prefer_system_libs", True))
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/runner.py
def install(self, version=None, downloader=None, callback=None):
    """Install runner using package management systems."""
    logger.debug(
        "Installing %s (version=%s, downloader=%s, callback=%s)",
        self.name,
        version,
        downloader,
        callback,
    )
    opts = {"downloader": downloader, "callback": callback}
    if self.download_url:
        opts["dest"] = os.path.join(settings.RUNNER_DIR, self.name)
        return self.download_and_extract(self.download_url, **opts)
    runner = self.get_runner_version(version)
    if not runner:
        raise RunnerInstallationError("Failed to retrieve {} ({}) information".format(self.name, version))
    if not downloader:
        raise RuntimeError("Missing mandatory downloader for runner %s" % self)

    if "wine" in self.name:
        opts["merge_single"] = True
        opts["dest"] = os.path.join(
            settings.RUNNER_DIR, self.name, "{}-{}".format(runner["version"], runner["architecture"])
        )

    if self.name == "libretro" and version:
        opts["merge_single"] = False
        opts["dest"] = os.path.join(settings.RUNNER_DIR, "retroarch/cores")
    self.download_and_extract(runner["url"], **opts)
install_dialog(self)

Ask the user if they want to install the runner.

Return success of runner installation.

Source code in lutris/runners/runner.py
def install_dialog(self):
    """Ask the user if they want to install the runner.

    Return success of runner installation.
    """
    dialog = dialogs.QuestionDialog(
        {
            "question": _("The required runner is not installed.\n"
                          "Do you wish to install it now?"),
            "title": _("Required runner unavailable"),
        }
    )
    if Gtk.ResponseType.YES == dialog.result:

        from lutris.gui.dialogs import ErrorDialog
        from lutris.gui.dialogs.download import simple_downloader
        try:
            if hasattr(self, "get_version"):
                version = self.get_version(use_default=False)  # pylint: disable=no-member
                self.install(downloader=simple_downloader, version=version)
            else:
                self.install(downloader=simple_downloader)
        except RunnerInstallationError as ex:
            ErrorDialog(ex.message)

        return self.is_installed()
    return False
is_installed(self)

Return whether the runner is installed

Source code in lutris/runners/runner.py
def is_installed(self):
    """Return whether the runner is installed"""
    return system.path_exists(self.get_executable())
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/runner.py
def prelaunch(self):
    """Run actions before running the game, override this method in runners"""
    available_libs = set()
    for lib in set(self.require_libs):
        if lib in LINUX_SYSTEM.shared_libraries:
            if self.arch:
                if self.arch in [_lib.arch for _lib in LINUX_SYSTEM.shared_libraries[lib]]:
                    available_libs.add(lib)
            else:
                available_libs.add(lib)
    unavailable_libs = set(self.require_libs) - available_libs
    if unavailable_libs:
        raise UnavailableLibraries(unavailable_libs, self.arch)
    return True
remove_game_data(app_id=None, game_path=None) staticmethod
Source code in lutris/runners/runner.py
@staticmethod
def remove_game_data(app_id=None, game_path=None):
    system.remove_folder(game_path)
run(self, *args)

Run the runner alone.

Source code in lutris/runners/runner.py
def run(self, *args):
    """Run the runner alone."""
    if not self.runnable_alone:
        return
    if not self.is_installed():
        if not self.install_dialog():
            logger.info("Runner install cancelled")
            return

    command_data = self.get_run_data()
    command = command_data.get("command")
    env = (command_data.get("env") or {}).copy()

    if hasattr(self, "prelaunch"):
        self.prelaunch()

    command_runner = MonitoredCommand(command, runner=self, env=env)
    command_runner.start()
uninstall(self)
Source code in lutris/runners/runner.py
def uninstall(self):
    runner_path = os.path.join(settings.RUNNER_DIR, self.name)
    if os.path.isdir(runner_path):
        system.remove_folder(runner_path)
use_runtime(self)
Source code in lutris/runners/runner.py
def use_runtime(self):
    if runtime.RUNTIME_DISABLED:
        logger.info("Runtime disabled by environment")
        return False
    if self.system_config.get("disable_runtime"):
        logger.info("Runtime disabled by system configuration")
        return False
    return True

ryujinx

ryujinx (Runner)

Source code in lutris/runners/ryujinx.py
class ryujinx(Runner):
    human_name = _("Ryujinx")
    platforms = [_("Nintendo Switch")]
    description = _("Nintendo Switch emulator")
    runnable_alone = True
    runner_executable = "ryujinx/publish/Ryujinx"
    download_url = "https://lutris.nyc3.digitaloceanspaces.com/runners/ryujinx/ryujinx-1.0.7074-linux_x64.tar.gz"

    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("NSP file"),
            "help": _("The game data, commonly called a ROM image."),
        }
    ]
    runner_options = [
        {
            "option": "prod_keys",
            "label": _("Encryption keys"),
            "type": "file",
            "help": _("File containing the encryption keys."),
        }, {
            "option": "title_keys",
            "label": _("Title keys"),
            "type": "file",
            "help": _("File containing the title keys."),
        }
    ]

    @property
    def ryujinx_data_dir(self):
        """Return dir where Ryujinx files lie."""
        candidates = ("~/.local/share/ryujinx", )
        for candidate in candidates:
            path = system.fix_path_case(os.path.join(os.path.expanduser(candidate), "nand"))
            if path and system.path_exists(path):
                return path[:-len("nand")]

    def play(self):
        """Run the game."""
        arguments = [self.get_executable()]
        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        arguments.append(rom)
        return {"command": arguments}

    def _update_key(self, key_type):
        """Update a keys file if set """
        ryujinx_data_dir = self.ryujinx_data_dir
        if not ryujinx_data_dir:
            logger.error("Ryujinx data dir not set")
            return
        if key_type == "prod_keys":
            key_loc = os.path.join(ryujinx_data_dir, "keys/prod.keys")
        elif key_type == "title_keys":
            key_loc = os.path.join(ryujinx_data_dir, "keys/title.keys")
        else:
            logger.error("Invalid keys type %s!", key_type)
            return

        key = self.runner_config.get(key_type)
        if not key:
            logger.debug("No %s file was set.", key_type)
            return
        if not system.path_exists(key):
            logger.warning("Keys file %s does not exist!", key)
            return

        keys_dir = os.path.dirname(key_loc)
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
        elif os.path.isfile(key_loc) and filecmp.cmp(key, key_loc):
            # If the files are identical, don't do anything
            return
        copyfile(key, key_loc)

    def prelaunch(self):
        for key in ["prod_keys", "title_keys"]:
            self._update_key(key_type=key)
        return True
description
download_url
game_options
human_name
platforms
runnable_alone
runner_executable
runner_options
ryujinx_data_dir property readonly

Return dir where Ryujinx files lie.

play(self)

Run the game.

Source code in lutris/runners/ryujinx.py
def play(self):
    """Run the game."""
    arguments = [self.get_executable()]
    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    arguments.append(rom)
    return {"command": arguments}
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/ryujinx.py
def prelaunch(self):
    for key in ["prod_keys", "title_keys"]:
        self._update_key(key_type=key)
    return True

scummvm

scummvm (Runner)

Source code in lutris/runners/scummvm.py
class scummvm(Runner):
    description = _("Engine for point-and-click games.")
    human_name = _("ScummVM")
    platforms = [_("Linux")]
    runnable_alone = True
    runner_executable = "scummvm/bin/scummvm"
    game_options = [
        {
            "option": "game_id",
            "type": "string",
            "label": _("Game identifier")
        },
        {
            "option": "path",
            "type": "directory_chooser",
            "label": _("Game files location")
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "help": _("Command line arguments used when launching the game"),
        },
    ]

    option_map = {
        "aspect": "--aspect-ratio",
        "subtitles": "--subtitles",
        "fullscreen": "--fullscreen",
        "gfx-mode": "--gfx-mode=%s",
        "scale-factor": "--scale-factor=%s",
        "render-mode": "--render-mode=%s",
        "filtering": "--filtering",
        "platform": "--platform=%s",
        "engine-speed": "--engine-speed=%s",
        "talk-speed": "--talkspeed=%s",
        "dimuse-tempo": "--dimuse-tempo=%s",
        "music-tempo": "--tempo=%s",
        "opl-driver": "--opl-driver=%s",
        "output-rate": "--output-rate=%s",
        "music-driver": "--music-driver=%s",
        "multi-midi": "--multi-midi",
        "midi-gain": "--midi-gain=%s",
        "soundfont": "--soundfont=%s",
        "music-volume": "--music-volume=%s",
        "sfx-volume": "--sfx-volume=%s",
        "speech-volume": "--speech-volume=%s",
        "native-mt32": "--native-mt32",
        "enable-gs": "--enable-gs",
        "joystick": "--joystick=%s",
        "language": "--language=%s",
        "alt-intro": "--alt-intro",
        "copy-protection": "--copy-protection",
        "demo-mode": "--demo-mode",
        "debug-level": "--debug-level=%s",
        "debug-flags": "--debug-flags=%s",
    }

    option_empty_map = {
        "fullscreen": "--no-fullscreen"
    }

    runner_options = [
        {
            "option": "fullscreen",
            "label": _("Fullscreen"),
            "type": "bool",
            "default": True,
        },
        {
            "option": "subtitles",
            "label": _("Enable subtitles"),
            "type": "bool",
            "default": False,
            "help": ("Enable subtitles for games with voice"),
        },
        {
            "option": "aspect",
            "label": _("Aspect ratio correction"),
            "type": "bool",
            "default": True,
            "help": _(
                "Most games supported by ScummVM were made for VGA "
                "display modes using rectangular pixels. Activating "
                "this option for these games will preserve the 4:3 "
                "aspect ratio they were made for."
            ),
        },
        {
            "option": "gfx-mode",
            "label": _("Graphic scaler"),
            "type": "choice",
            "default": "3x",
            "choices": [
                ("1x", "1x"),
                ("2x", "2x"),
                ("3x", "3x"),
                ("hq2x", "hq2x"),
                ("hq3x", "hq3x"),
                ("advmame2x", "advmame2x"),
                ("advmame3x", "advmame3x"),
                ("2xsai", "2xsai"),
                ("super2xsai", "super2xsai"),
                ("supereagle", "supereagle"),
                ("tv2x", "tv2x"),
                ("dotmatrix", "dotmatrix"),
            ],
            "help":
            _("The algorithm used to scale up the game's base "
              "resolution, resulting in different visual styles. "),
        },
        # {
        #    "option": "scale-factor",
        #    "label": _("Scaler factor"),
        #    "type": "choice",
        #    "choices": [
        #        ("1", "1"),
        #        ("2", "2"),
        #        ("3", "3"),
        #        ("4", "4"),
        #        ("5", "5"),
        #    ],
        #    "help":
        #    _("Changes the resolution of the game. "
        #      "For example, a 2x scaler will take a 320x200 "
        #      "resolution game and scale it up to 640x400. "),
        # },
        {
            "option": "render-mode",
            "label": _("Render mode"),
            "type": "choice",
            "choices": [
                ("hercGreen", "hercGreen"),
                ("hercAmber", "hercAmber"),
                ("cga", "cga"),
                ("ega", "ega"),
                ("vga", "vga"),
                ("amiga", "amiga"),
                ("fmtowns", "fmtowns"),
                ("pc9821", "pc9821"),
                ("pc9801", "pc9801"),
                ("2gs", "2gs"),
                ("atari", "atari"),
                ("macintosh", "macintosh"),
            ],
            "advanced": True,
            "help": _("Changes how the game is rendered."),
        },
        {
            "option": "filtering",
            "label": _("Filtering"),
            "type": "bool",
            "help": _("Uses bilinear interpolation instead of nearest neighbor "
                      "resampling for the aspect ratio correction and stretch mode."),
            "default": False,
            "advanced": True,
        },
        {
            "option": "datadir",
            "label": _("Data directory"),
            "type": "directory_chooser",
            "help": _("Defaults to share/scummvm if unspecified."),
            "advanced": True,
        },
        {
            "option": "platform",
            "type": "string",
            "label": _("Platform"),
            "help": _("Specifes platform of game. Allowed values: 2gs, 3do, acorn, amiga, atari, c64, "
                      "fmtowns, nes, mac, pc pc98, pce, segacd, wii, windows"),
            "advanced": True,
        },
        {
            "option": "joystick",
            "type": "string",
            "label": _("Joystick"),
            "help": _("Enables joystick input (default: 0 = first joystick)"),
            "advanced": True,
        },
        {
            "option": "language",
            "type": "string",
            "label": _("Language"),
            "help": _("Selects language (en, de, fr, it, pt, es, jp, zh, kr, se, gb, hb, ru, cz)"),
            "advanced": True,
        },
        {
            "option": "engine-speed",
            "type": "string",
            "label": _("Engine speed"),
            "help": _("Sets frames per second limit (0 - 100) for Grim Fandango "
                      "or Escape from Monkey Island (default: 60)."),
            "advanced": True,
        },
        {
            "option": "talk-speed",
            "type": "string",
            "label": _("Talk speed"),
            "help": _("Sets talk speed for games (default: 60)"),
            "advanced": True,
        },
        {
            "option": "music-tempo",
            "type": "string",
            "label": _("Music tempo"),
            "help": _("Sets music tempo (in percent, 50-200) for SCUMM games (default: 100)"),
            "advanced": True,
        },
        {
            "option": "dimuse-tempo",
            "type": "string",
            "label": _("Digital iMuse tempo"),
            "help": _("Sets internal Digital iMuse tempo (10 - 100) per second (default: 10)"),
            "advanced": True,
        },
        {
            "option": "music-driver",
            "label": _("Music driver"),
            "type": "choice",
            "choices": [
                ("null", "null"),
                ("auto", "auto"),
                ("seq", "seq"),
                ("sndio", "sndio"),
                ("alsa", "alsa"),
                ("fluidsynth", "fluidsynth"),
                ("mt32", "mt32"),
                ("adlib", "adlib"),
                ("pcspk", "pcspk"),
                ("pcjr", "pcjr"),
                ("cms", "cms"),
                ("timidity", "timidity"),
            ],
            "help": _("Specifies the device ScummVM uses to output audio."),
            "advanced": True,
        },
        {
            "option": "output-rate",
            "label": _("Output rate"),
            "type": "choice",
            "choices": [
                ("11025", "11025"),
                ("22050", "22050"),
                ("44100", "44100"),
            ],
            "help": _("Selects output sample rate in Hz."),
            "advanced": True,
        },
        {
            "option": "opl-driver",
            "label": _("OPL driver"),
            "type": "choice",
            "choices": [
                ("auto", "auto"),
                ("mame", "mame"),
                ("db", "db"),
                ("nuked", "nuked"),
                ("alsa", "alsa"),
                ("op2lpt", "op2lpt"),
                ("op3lpt", "op3lpt"),
                ("rwopl3", "rwopl3"),
            ],
            "help": _("Chooses which emulator is used by ScummVM when the AdLib emulator "
                      "is chosen as the Preferred device."),
            "advanced": True,
        },
        {
            "option": "music-volume",
            "type": "string",
            "label": _("Music volume"),
            "help": _("Sets the music volume, 0-255 (default: 192)"),
            "advanced": True,
        },
        {
            "option": "sfx-volume",
            "type": "string",
            "label": _("SFX volume"),
            "help": _("Sets the sfx volume, 0-255 (default: 192)"),
            "advanced": True,
        },
        {
            "option": "speech-volume",
            "type": "string",
            "label": _("Speech volume"),
            "help": _("Sets the speech volume, 0-255 (default: 192)"),
            "advanced": True,
        },
        {
            "option": "midi-gain",
            "type": "string",
            "label": _("MIDI gain"),
            "help": _("Sets the gain for MIDI playback. 0-1000 (default: 100)"),
            "advanced": True,
        },
        {
            "option": "soundfont",
            "type": "string",
            "label": _("Soundfont"),
            "help": _("Specifies the path to a soundfont file."),
            "advanced": True,
        },
        {
            "option": "multi-midi",
            "label": _("Mixed AdLib/MIDI mode"),
            "type": "bool",
            "default": False,
            "help": _("Combines MIDI music with AdLib sound effects."),
            "advanced": True,
        },
        {
            "option": "native-mt32",
            "label": _("True Roland MT-32"),
            "type": "bool",
            "default": False,
            "help": _("Tells ScummVM that the MIDI device is an actual Roland MT-32, "
                      "LAPC-I, CM-64, CM-32L, CM-500 or other MT-32 device."),
            "advanced": True,
        },
        {
            "option": "enable-gs",
            "label": _("Enable Roland GS"),
            "type": "bool",
            "default": False,
            "help": _("Tells ScummVM that the MIDI device is a GS device that has "
                      "an MT-32 map, such as an SC-55, SC-88 or SC-8820."),
            "advanced": True,
        },
        {
            "option": "alt-intro",
            "type": "bool",
            "label": _("Use alternate intro"),
            "help": _("Uses alternative intro for CD versions"),
            "advanced": True,
        },
        {
            "option": "copy-protection",
            "type": "bool",
            "label": _("Copy protection"),
            "help": _("Enables copy protection"),
            "advanced": True,
        },
        {
            "option": "demo-mode",
            "type": "bool",
            "label": _("Demo mode"),
            "help": _("Starts demo mode of Maniac Mansion or The 7th Guest"),
            "advanced": True,
        },
        {
            "option": "debug-level",
            "type": "string",
            "label": _("Debug level"),
            "help": _("Sets debug verbosity level"),
            "advanced": True,
        },
        {
            "option": "debug-flags",
            "type": "string",
            "label": _("Debug flags"),
            "help": _("Enables engine specific debug flags"),
            "advanced": True,
        },
    ]

    @property
    def game_path(self):
        return self.game_config.get("path")

    @property
    def libs_dir(self):
        path = os.path.join(settings.RUNNER_DIR, "scummvm/lib")
        return path if system.path_exists(path) else ""

    def get_command(self):
        return [
            self.get_executable(),
            "--extrapath=%s" % self.get_scummvm_data_dir(),
            "--themepath=%s" % self.get_scummvm_data_dir(),
        ]

    def get_scummvm_data_dir(self):
        data_dir = self.runner_config.get("datadir")

        if data_dir is None:
            root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
            data_dir = os.path.join(root_dir, "share/scummvm")

        return data_dir

    def get_run_data(self):
        env = self.get_env()
        env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
            self.libs_dir,
            env.get("LD_LIBRARY_PATH")]))
        return {"env": env, "command": self.get_command()}

    def inject_runner_option(self, command, key, cmdline, cmdline_empty=None):
        value = self.runner_config.get(key)
        if value:
            if "%s" in cmdline:
                command.append(cmdline % value)
            else:
                command.append(cmdline)
        elif cmdline_empty:
            command.append(cmdline_empty)

    def play(self):
        command = self.get_command()
        for option, cmdline in self.option_map.items():
            self.inject_runner_option(command, option, cmdline, self.option_empty_map.get(option))
        command.append("--path=%s" % self.game_path)
        args = self.game_config.get("args") or ""
        for arg in split_arguments(args):
            command.append(arg)
        command.append(self.game_config.get("game_id"))
        return {"command": command, "ld_library_path": self.libs_dir}

    def get_game_list(self):
        """Return the entire list of games supported by ScummVM."""
        with subprocess.Popen([self.get_executable(), "--list-games"],
                              stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as scummvm_process:
            scumm_output = scummvm_process.communicate()[0]
            game_list = str.split(scumm_output, "\n")
        game_array = []
        game_list_start = False
        for game in game_list:
            if game_list_start:
                if len(game) > 1:
                    dir_limit = game.index(" ")
                else:
                    dir_limit = None
                if dir_limit is not None:
                    game_dir = game[0:dir_limit]
                    game_name = game[dir_limit + 1:len(game)].strip()
                    game_array.append([game_dir, game_name])
            # The actual list is below a separator
            if game.startswith("-----"):
                game_list_start = True
        return game_array
description
game_options
game_path property readonly

Return the directory where the game is installed.

human_name
libs_dir property readonly
option_empty_map
option_map
platforms
runnable_alone
runner_executable
runner_options
get_command(self)
Source code in lutris/runners/scummvm.py
def get_command(self):
    return [
        self.get_executable(),
        "--extrapath=%s" % self.get_scummvm_data_dir(),
        "--themepath=%s" % self.get_scummvm_data_dir(),
    ]
get_game_list(self)

Return the entire list of games supported by ScummVM.

Source code in lutris/runners/scummvm.py
def get_game_list(self):
    """Return the entire list of games supported by ScummVM."""
    with subprocess.Popen([self.get_executable(), "--list-games"],
                          stdout=subprocess.PIPE, encoding="utf-8", universal_newlines=True) as scummvm_process:
        scumm_output = scummvm_process.communicate()[0]
        game_list = str.split(scumm_output, "\n")
    game_array = []
    game_list_start = False
    for game in game_list:
        if game_list_start:
            if len(game) > 1:
                dir_limit = game.index(" ")
            else:
                dir_limit = None
            if dir_limit is not None:
                game_dir = game[0:dir_limit]
                game_name = game[dir_limit + 1:len(game)].strip()
                game_array.append([game_dir, game_name])
        # The actual list is below a separator
        if game.startswith("-----"):
            game_list_start = True
    return game_array
get_run_data(self)

Return dict with command (exe & args list) and env vars (dict).

Reimplement in derived runner if need be.

Source code in lutris/runners/scummvm.py
def get_run_data(self):
    env = self.get_env()
    env["LD_LIBRARY_PATH"] = os.pathsep.join(filter(None, [
        self.libs_dir,
        env.get("LD_LIBRARY_PATH")]))
    return {"env": env, "command": self.get_command()}
get_scummvm_data_dir(self)
Source code in lutris/runners/scummvm.py
def get_scummvm_data_dir(self):
    data_dir = self.runner_config.get("datadir")

    if data_dir is None:
        root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
        data_dir = os.path.join(root_dir, "share/scummvm")

    return data_dir
inject_runner_option(self, command, key, cmdline, cmdline_empty=None)
Source code in lutris/runners/scummvm.py
def inject_runner_option(self, command, key, cmdline, cmdline_empty=None):
    value = self.runner_config.get(key)
    if value:
        if "%s" in cmdline:
            command.append(cmdline % value)
        else:
            command.append(cmdline)
    elif cmdline_empty:
        command.append(cmdline_empty)
play(self)
Source code in lutris/runners/scummvm.py
def play(self):
    command = self.get_command()
    for option, cmdline in self.option_map.items():
        self.inject_runner_option(command, option, cmdline, self.option_empty_map.get(option))
    command.append("--path=%s" % self.game_path)
    args = self.game_config.get("args") or ""
    for arg in split_arguments(args):
        command.append(arg)
    command.append(self.game_config.get("game_id"))
    return {"command": command, "ld_library_path": self.libs_dir}

snes9x

SNES9X_DIR

snes9x (Runner)

Source code in lutris/runners/snes9x.py
class snes9x(Runner):
    description = _("Super Nintendo emulator")
    human_name = _("Snes9x")
    platforms = [_("Nintendo SNES")]
    runnable_alone = True
    runner_executable = "snes9x/bin/snes9x-gtk"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "default_path": "game_path",
            "label": _("ROM file"),
            "help": _("The game data, commonly called a ROM image."),
        }
    ]

    runner_options = [
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": "1"
        },
        {
            "option":
            "maintain_aspect_ratio",
            "type":
            "bool",
            "label":
            _("Maintain aspect ratio (4:3)"),
            "default":
            "1",
            "help": _(
                "Super Nintendo games were made for 4:3 "
                "screens with rectangular pixels, but modern screens "
                "have square pixels, which results in a vertically "
                "squeezed image. This option corrects this by displaying "
                "rectangular pixels."
            ),
        },
        {
            "option": "sound_driver",
            "type": "choice",
            "label": _("Sound driver"),
            "advanced": True,
            "choices": (("SDL", "1"), ("ALSA", "2"), ("OSS", "0")),
            "default": "1",
        },
    ]

    def set_option(self, option, value):
        config_file = os.path.expanduser("~/.snes9x/snes9x.xml")
        if not system.path_exists(config_file):
            with subprocess.Popen([self.get_executable(), "-help"]) as snes9x_process:
                snes9x_process.communicate()
        if not system.path_exists(config_file):
            logger.error("Snes9x config file creation failed")
            return
        tree = etree.parse(config_file)
        node = tree.find("./preferences/option[@name='%s']" % option)
        if value.__class__.__name__ == "bool":
            value = "1" if value else "0"
        node.attrib["value"] = value
        tree.write(config_file)

    def play(self):
        for option_name in self.config.runner_config:
            self.set_option(option_name, self.runner_config.get(option_name))

        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        return {"command": [self.get_executable(), rom]}
description
game_options
human_name
platforms
runnable_alone
runner_executable
runner_options
play(self)
Source code in lutris/runners/snes9x.py
def play(self):
    for option_name in self.config.runner_config:
        self.set_option(option_name, self.runner_config.get(option_name))

    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    return {"command": [self.get_executable(), rom]}
set_option(self, option, value)
Source code in lutris/runners/snes9x.py
def set_option(self, option, value):
    config_file = os.path.expanduser("~/.snes9x/snes9x.xml")
    if not system.path_exists(config_file):
        with subprocess.Popen([self.get_executable(), "-help"]) as snes9x_process:
            snes9x_process.communicate()
    if not system.path_exists(config_file):
        logger.error("Snes9x config file creation failed")
        return
    tree = etree.parse(config_file)
    node = tree.find("./preferences/option[@name='%s']" % option)
    if value.__class__.__name__ == "bool":
        value = "1" if value else "0"
    node.attrib["value"] = value
    tree.write(config_file)

steam

Steam for Linux runner

steam (Runner)

Source code in lutris/runners/steam.py
class steam(Runner):
    description = _("Runs Steam for Linux games")
    human_name = _("Steam")
    platforms = [_("Linux")]
    runner_executable = "steam"
    game_options = [
        {
            "option": "appid",
            "label": _("Application ID"),
            "type": "string",
            "help": _(
                "The application ID can be retrieved from the game's "
                "page at steampowered.com. Example: 235320 is the "
                "app ID for <i>Original War</i> in: \n"
                "http://store.steampowered.com/app/<b>235320</b>/"
            ),
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "help": _(
                "Command line arguments used when launching the game.\n"
                "Ignored when Steam Big Picture mode is enabled."
            ),
        },
        {
            "option": "run_without_steam",
            "label": _("DRM free mode (Do not launch Steam)"),
            "type": "bool",
            "default": False,
            "advanced": True,
            "help": _(
                "Run the game directly without Steam, requires the game binary path to be set"
            ),
        },
        {
            "option": "steamless_binary",
            "type": "file",
            "label": _("Game binary path"),
            "advanced": True,
            "help": _("Path to the game executable (Required by DRM free mode)"),
        },
    ]
    runner_options = [
        {
            "option": "quit_steam_on_exit",
            "label": _("Stop Steam after game exits"),
            "type": "bool",
            "default": False,
            "help": _(
                "Shut down Steam after the game has quit\n"
                "(only if Steam was started by Lutris)"
            ),
        },
        {
            "option": "start_in_big_picture",
            "label": _("Start Steam in Big Picture mode"),
            "type": "bool",
            "default": False,
            "help": _(
                "Launches Steam in Big Picture mode.\n"
                "Only works if Steam is not running or "
                "already running in Big Picture mode.\n"
                "Useful when playing with a Steam Controller."
            ),
        },
        {
            "option": "lsi_steam",
            "label": _("Start Steam with LSI"),
            "type": "bool",
            "default": False,
            "help": _(
                "Launches steam with LSI patches enabled. "
                "Make sure Lutris Runtime is disabled and "
                "you have LSI installed. "
                "https://github.com/solus-project/linux-steam-integration"
            ),
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "advanced": True,
            "help": _("Extra command line arguments used when launching Steam"),
        },
    ]
    system_options_override = [{"option": "disable_runtime", "default": True}]

    def __init__(self, config=None):
        super().__init__(config)
        self.own_game_remove_method = _("Remove game data (through Steam)")
        self.no_game_remove_warning = True
        self.original_steampid = None

    @property
    def runnable_alone(self):
        return not linux.LINUX_SYSTEM.is_flatpak

    @property
    def appid(self):
        return self.game_config.get("appid") or ""

    def get_steam_config(self):
        """Return the "Steam" part of Steam's config.vdf as a dict."""
        return read_config(self.steam_data_dir)

    def get_library_config(self):
        """Return the "libraryfolders" part of Steam's libraryfolders.vdf as a dict """
        return read_library_folders(self.steam_data_dir)

    @property
    def game_path(self):
        if not self.appid:
            return None
        return self.get_game_path_from_appid(self.appid)

    @property
    def steam_data_dir(self):
        """Main installation directory for Steam"""
        return get_steam_dir()

    @property
    def library_folders(self):
        """Return a list Steam library paths"""
        return self.get_steamapps_dirs()

    def get_appmanifest(self):
        """Return an AppManifest instance for the game"""
        appmanifests = []
        for apps_path in self.get_steamapps_dirs():
            appmanifest = get_appmanifest_from_appid(apps_path, self.appid)
            if appmanifest:
                appmanifests.append(appmanifest)
        if len(appmanifests) > 1:
            logger.warning("More than one AppManifest for %s returning only 1st", self.appid)
        if appmanifests:
            return appmanifests[0]

    def get_executable(self):
        if linux.LINUX_SYSTEM.is_flatpak:
            # Use xdg-open for Steam URIs in Flatpak
            return system.find_executable("xdg-open")
        if self.runner_config.get("lsi_steam") and system.find_executable("lsi-steam"):
            return system.find_executable("lsi-steam")
        runner_executable = self.runner_config.get("runner_executable")
        if runner_executable and os.path.isfile(runner_executable):
            return runner_executable
        return system.find_executable(self.runner_executable)

    @property
    def working_dir(self):
        """Return the working directory to use when running the game."""
        if self.game_config.get("run_without_steam"):
            steamless_binary = self.game_config.get("steamless_binary")
            if steamless_binary and os.path.isfile(steamless_binary):
                return os.path.dirname(steamless_binary)
        return super().working_dir

    @property
    def launch_args(self):
        """Provide launch arguments for Steam"""
        args = [self.get_executable()]
        if linux.LINUX_SYSTEM.is_flatpak:
            return args
        if self.runner_config.get("start_in_big_picture"):
            args.append("-bigpicture")
        return args + split_arguments(self.runner_config.get("args") or "")

    def get_game_path_from_appid(self, appid):
        """Return the game directory."""
        for apps_path in self.get_steamapps_dirs():
            game_path = get_path_from_appmanifest(apps_path, appid)
            if game_path:
                return game_path
        logger.info("Data path for SteamApp %s not found.", appid)

    def get_steamapps_dirs(self):
        """Return a list of the Steam library main + custom folders."""
        dirs = []
        # Extra colon-separated compatibility tools dirs environment variable
        if 'STEAM_EXTRA_COMPAT_TOOLS_PATHS' in os.environ:
            dirs += os.getenv('STEAM_EXTRA_COMPAT_TOOLS_PATHS').split(':')
        # Main steamapps dir and compatibilitytools.d dir
        for data_dir in STEAM_DATA_DIRS:
            for _dir in ["steamapps", "compatibilitytools.d"]:
                abs_dir = os.path.join(os.path.expanduser(data_dir), _dir)
                abs_dir = system.fix_path_case(abs_dir)
                if abs_dir and os.path.isdir(abs_dir):
                    dirs.append(abs_dir)

        # Custom dirs
        steam_config = self.get_steam_config()
        if steam_config:
            i = 1
            while "BaseInstallFolder_%s" % i in steam_config:
                path = steam_config["BaseInstallFolder_%s" % i] + "/steamapps"
                path = system.fix_path_case(path)
                if path and os.path.isdir(path):
                    dirs.append(path)
                i += 1

        # New Custom dirs
        library_config = self.get_library_config()
        if library_config:
            paths = []
            for entry in library_config.values():
                if "mounted" in entry:
                    if entry.get("path") and entry.get("mounted") == "1":
                        path = system.fix_path_case(entry.get("path") + "/steamapps")
                        paths.append(path)
                else:
                    path = system.fix_path_case(entry.get("path") + "/steamapps")
                    paths.append(path)
            for path in paths:
                if path and os.path.isdir(path):
                    dirs.append(path)
        return system.list_unique_folders(dirs)

    def get_default_steamapps_path(self):
        steamapps_paths = self.get_steamapps_dirs()
        if steamapps_paths:
            return steamapps_paths[0]

    def install(self, version=None, downloader=None, callback=None):
        raise NonInstallableRunnerError(
            "Steam for Linux installation is not handled by Lutris.\n"
            "Please go to "
            "<a href='http://steampowered.com'>http://steampowered.com</a>"
            " or install Steam with the package provided by your distribution."
        )

    def install_game(self, appid, generate_acf=False):
        logger.debug("Installing steam game %s", appid)
        if generate_acf:
            acf_data = get_default_acf(appid, appid)
            acf_content = to_vdf(acf_data)
            steamapps_path = self.get_default_steamapps_path()
            if not steamapps_path:
                raise RuntimeError("Could not find Steam path, is Steam installed?")
            acf_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
            with open(acf_path, "w", encoding='utf-8') as acf_file:
                acf_file.write(acf_content)
            if is_running():
                shutdown()
                time.sleep(5)
        command = [self.get_executable(), "steam://install/%s" % appid]
        subprocess.Popen(command)  # pylint: disable=consider-using-with

    def prelaunch(self):
        def has_steam_shutdown(times=10):
            for __ in range(times):
                time.sleep(1)
                if not is_running():
                    return True

        # If using primusrun, shutdown existing Steam first
        if self.system_config.get("optimus") != "off" and is_running():
            shutdown()
            if not has_steam_shutdown():
                logger.info("Forcing Steam shutdown")
                kill()
                if not has_steam_shutdown(5):
                    logger.error("Failed to shut down Steam :(")
                    return False
        return True

    def get_run_data(self):
        return {"command": self.launch_args, "env": self.get_env()}

    def play(self):
        game_args = self.game_config.get("args") or ""

        binary_path = self.game_config.get("steamless_binary")
        if self.game_config.get("run_without_steam") and binary_path:
            # Start without steam
            if not system.path_exists(binary_path):
                return {"error": "FILE_NOT_FOUND", "file": binary_path}
            self.original_steampid = None
            command = [binary_path]
        else:
            # Start through steam
            if linux.LINUX_SYSTEM.is_flatpak:
                if game_args:
                    steam_uri = "steam://run/%s//%s/" % (self.appid, game_args)
                else:
                    steam_uri = "steam://rungameid/%s" % self.appid
                return {
                    "command": self.launch_args + [steam_uri],
                    "env": self.get_env(),
                }

            # Get current steam pid to act as the root pid instead of lutris
            self.original_steampid = get_steam_pid()
            command = self.launch_args

            if self.runner_config.get("start_in_big_picture") or not game_args:
                command.append("steam://rungameid/%s" % self.appid)
            else:
                command.append("-applaunch")
                command.append(self.appid)

        if game_args:
            for arg in split_arguments(game_args):
                command.append(arg)

        return {
            "command": command,
            "env": self.get_env(),
        }

    def stop(self):
        if self.runner_config.get("quit_steam_on_exit") and not self.original_steampid:
            shutdown()
            return True
        return False

    def remove_game_data(self, appid=None, **kwargs):
        if not self.is_installed():
            return False
        command = MonitoredCommand(
            [self.get_executable(), "steam://uninstall/%s" % (appid or self.appid)],
            runner=self,
            env=self.get_env(),
        )
        command.start()
appid property readonly
description
game_options
game_path property readonly

Return the directory where the game is installed.

human_name
launch_args property readonly

Provide launch arguments for Steam

library_folders property readonly

Return a list Steam library paths

platforms
runnable_alone property readonly

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

runner_executable
runner_options
steam_data_dir property readonly

Main installation directory for Steam

system_options_override
working_dir property readonly

Return the working directory to use when running the game.

__init__(self, config=None) special
Source code in lutris/runners/steam.py
def __init__(self, config=None):
    super().__init__(config)
    self.own_game_remove_method = _("Remove game data (through Steam)")
    self.no_game_remove_warning = True
    self.original_steampid = None
get_appmanifest(self)

Return an AppManifest instance for the game

Source code in lutris/runners/steam.py
def get_appmanifest(self):
    """Return an AppManifest instance for the game"""
    appmanifests = []
    for apps_path in self.get_steamapps_dirs():
        appmanifest = get_appmanifest_from_appid(apps_path, self.appid)
        if appmanifest:
            appmanifests.append(appmanifest)
    if len(appmanifests) > 1:
        logger.warning("More than one AppManifest for %s returning only 1st", self.appid)
    if appmanifests:
        return appmanifests[0]
get_default_steamapps_path(self)
Source code in lutris/runners/steam.py
def get_default_steamapps_path(self):
    steamapps_paths = self.get_steamapps_dirs()
    if steamapps_paths:
        return steamapps_paths[0]
get_executable(self)
Source code in lutris/runners/steam.py
def get_executable(self):
    if linux.LINUX_SYSTEM.is_flatpak:
        # Use xdg-open for Steam URIs in Flatpak
        return system.find_executable("xdg-open")
    if self.runner_config.get("lsi_steam") and system.find_executable("lsi-steam"):
        return system.find_executable("lsi-steam")
    runner_executable = self.runner_config.get("runner_executable")
    if runner_executable and os.path.isfile(runner_executable):
        return runner_executable
    return system.find_executable(self.runner_executable)
get_game_path_from_appid(self, appid)

Return the game directory.

Source code in lutris/runners/steam.py
def get_game_path_from_appid(self, appid):
    """Return the game directory."""
    for apps_path in self.get_steamapps_dirs():
        game_path = get_path_from_appmanifest(apps_path, appid)
        if game_path:
            return game_path
    logger.info("Data path for SteamApp %s not found.", appid)
get_library_config(self)

Return the "libraryfolders" part of Steam's libraryfolders.vdf as a dict

Source code in lutris/runners/steam.py
def get_library_config(self):
    """Return the "libraryfolders" part of Steam's libraryfolders.vdf as a dict """
    return read_library_folders(self.steam_data_dir)
get_run_data(self)

Return dict with command (exe & args list) and env vars (dict).

Reimplement in derived runner if need be.

Source code in lutris/runners/steam.py
def get_run_data(self):
    return {"command": self.launch_args, "env": self.get_env()}
get_steam_config(self)

Return the "Steam" part of Steam's config.vdf as a dict.

Source code in lutris/runners/steam.py
def get_steam_config(self):
    """Return the "Steam" part of Steam's config.vdf as a dict."""
    return read_config(self.steam_data_dir)
get_steamapps_dirs(self)

Return a list of the Steam library main + custom folders.

Source code in lutris/runners/steam.py
def get_steamapps_dirs(self):
    """Return a list of the Steam library main + custom folders."""
    dirs = []
    # Extra colon-separated compatibility tools dirs environment variable
    if 'STEAM_EXTRA_COMPAT_TOOLS_PATHS' in os.environ:
        dirs += os.getenv('STEAM_EXTRA_COMPAT_TOOLS_PATHS').split(':')
    # Main steamapps dir and compatibilitytools.d dir
    for data_dir in STEAM_DATA_DIRS:
        for _dir in ["steamapps", "compatibilitytools.d"]:
            abs_dir = os.path.join(os.path.expanduser(data_dir), _dir)
            abs_dir = system.fix_path_case(abs_dir)
            if abs_dir and os.path.isdir(abs_dir):
                dirs.append(abs_dir)

    # Custom dirs
    steam_config = self.get_steam_config()
    if steam_config:
        i = 1
        while "BaseInstallFolder_%s" % i in steam_config:
            path = steam_config["BaseInstallFolder_%s" % i] + "/steamapps"
            path = system.fix_path_case(path)
            if path and os.path.isdir(path):
                dirs.append(path)
            i += 1

    # New Custom dirs
    library_config = self.get_library_config()
    if library_config:
        paths = []
        for entry in library_config.values():
            if "mounted" in entry:
                if entry.get("path") and entry.get("mounted") == "1":
                    path = system.fix_path_case(entry.get("path") + "/steamapps")
                    paths.append(path)
            else:
                path = system.fix_path_case(entry.get("path") + "/steamapps")
                paths.append(path)
        for path in paths:
            if path and os.path.isdir(path):
                dirs.append(path)
    return system.list_unique_folders(dirs)
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/steam.py
def install(self, version=None, downloader=None, callback=None):
    raise NonInstallableRunnerError(
        "Steam for Linux installation is not handled by Lutris.\n"
        "Please go to "
        "<a href='http://steampowered.com'>http://steampowered.com</a>"
        " or install Steam with the package provided by your distribution."
    )
install_game(self, appid, generate_acf=False)
Source code in lutris/runners/steam.py
def install_game(self, appid, generate_acf=False):
    logger.debug("Installing steam game %s", appid)
    if generate_acf:
        acf_data = get_default_acf(appid, appid)
        acf_content = to_vdf(acf_data)
        steamapps_path = self.get_default_steamapps_path()
        if not steamapps_path:
            raise RuntimeError("Could not find Steam path, is Steam installed?")
        acf_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
        with open(acf_path, "w", encoding='utf-8') as acf_file:
            acf_file.write(acf_content)
        if is_running():
            shutdown()
            time.sleep(5)
    command = [self.get_executable(), "steam://install/%s" % appid]
    subprocess.Popen(command)  # pylint: disable=consider-using-with
play(self)
Source code in lutris/runners/steam.py
def play(self):
    game_args = self.game_config.get("args") or ""

    binary_path = self.game_config.get("steamless_binary")
    if self.game_config.get("run_without_steam") and binary_path:
        # Start without steam
        if not system.path_exists(binary_path):
            return {"error": "FILE_NOT_FOUND", "file": binary_path}
        self.original_steampid = None
        command = [binary_path]
    else:
        # Start through steam
        if linux.LINUX_SYSTEM.is_flatpak:
            if game_args:
                steam_uri = "steam://run/%s//%s/" % (self.appid, game_args)
            else:
                steam_uri = "steam://rungameid/%s" % self.appid
            return {
                "command": self.launch_args + [steam_uri],
                "env": self.get_env(),
            }

        # Get current steam pid to act as the root pid instead of lutris
        self.original_steampid = get_steam_pid()
        command = self.launch_args

        if self.runner_config.get("start_in_big_picture") or not game_args:
            command.append("steam://rungameid/%s" % self.appid)
        else:
            command.append("-applaunch")
            command.append(self.appid)

    if game_args:
        for arg in split_arguments(game_args):
            command.append(arg)

    return {
        "command": command,
        "env": self.get_env(),
    }
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/steam.py
def prelaunch(self):
    def has_steam_shutdown(times=10):
        for __ in range(times):
            time.sleep(1)
            if not is_running():
                return True

    # If using primusrun, shutdown existing Steam first
    if self.system_config.get("optimus") != "off" and is_running():
        shutdown()
        if not has_steam_shutdown():
            logger.info("Forcing Steam shutdown")
            kill()
            if not has_steam_shutdown(5):
                logger.error("Failed to shut down Steam :(")
                return False
    return True
remove_game_data(self, appid=None, **kwargs)
Source code in lutris/runners/steam.py
def remove_game_data(self, appid=None, **kwargs):
    if not self.is_installed():
        return False
    command = MonitoredCommand(
        [self.get_executable(), "steam://uninstall/%s" % (appid or self.appid)],
        runner=self,
        env=self.get_env(),
    )
    command.start()
stop(self)
Source code in lutris/runners/steam.py
def stop(self):
    if self.runner_config.get("quit_steam_on_exit") and not self.original_steampid:
        shutdown()
        return True
    return False

get_steam_pid()

Return pid of Steam process.

Source code in lutris/runners/steam.py
def get_steam_pid():
    """Return pid of Steam process."""
    return system.get_pid("steam$")

is_running()

Checks if Steam is running.

Source code in lutris/runners/steam.py
def is_running():
    """Checks if Steam is running."""
    return bool(get_steam_pid())

kill()

Force quit Steam.

Source code in lutris/runners/steam.py
def kill():
    """Force quit Steam."""
    system.kill_pid(get_steam_pid())

shutdown()

Cleanly quit Steam.

Source code in lutris/runners/steam.py
def shutdown():
    """Cleanly quit Steam."""
    logger.debug("Shutting down Steam")
    if is_running():
        subprocess.call(["steam", "-shutdown"])

vice

vice (Runner)

Source code in lutris/runners/vice.py
class vice(Runner):
    description = _("Commodore Emulator")
    human_name = _("Vice")
    platforms = [
        _("Commodore 64"),
        _("Commodore 128"),
        _("Commodore VIC20"),
        _("Commodore PET"),
        _("Commodore Plus/4"),
        _("Commodore CBM II"),
    ]
    machine_choices = [
        ("C64", "c64"),
        ("C128", "c128"),
        ("vic20", "vic20"),
        ("PET", "pet"),
        ("Plus/4", "plus4"),
        ("CBM-II", "cbmii"),
    ]
    game_options = [
        {
            "option":
            "main_file",
            "type":
            "file",
            "label":
            _("ROM file"),
            "help": _(
                "The game data, commonly called a ROM image.\n"
                "Supported formats: X64, D64, G64, P64, D67, D71, D81, "
                "D80, D82, D1M, D2M, D4M, T46, P00 and CRT."
            ),
        }
    ]

    runner_options = [
        {
            "option": "joy",
            "type": "bool",
            "label": _("Use joysticks"),
            "default": False
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": _("Fullscreen"),
            "default": False,
        },
        {
            "option": "double",
            "type": "bool",
            "label": _("Scale up display by 2"),
            "default": True,
        },
        {
            "option": "aspect_ratio",
            "type": "bool",
            "label": _("Preserve aspect ratio"),
            "default": True,
        },
        {
            "option": "drivesound",
            "type": "bool",
            "label": _("Enable sound emulation of disk drives"),
            "default": False,
        },
        {
            "option": "renderer",
            "type": "choice",
            "label": _("Graphics renderer"),
            "choices": [("OpenGL", "opengl"), (_("Software"), "software")],
            "default": "opengl",
        },
        {
            "option": "machine",
            "type": "choice",
            "label": _("Machine"),
            "choices": machine_choices,
            "default": "c64",
        },
    ]

    def get_platform(self):
        machine = self.game_config.get("machine")
        if machine:
            for index, choice in enumerate(self.machine_choices):
                if choice[1] == machine:
                    return self.platforms[index]
        return self.platforms[0]  # Default to C64

    def get_executable(self, machine=None):
        if not machine:
            machine = "c64"
        executables = {
            "c64": "x64",
            "c128": "x128",
            "vic20": "xvic",
            "pet": "xpet",
            "plus4": "xplus4",
            "cbmii": "xcbm2",
        }
        try:
            executable = executables[machine]
        except KeyError as ex:
            raise ValueError("Invalid machine '%s'" % machine) from ex
        return os.path.join(settings.RUNNER_DIR, "vice/bin/%s" % executable)

    def install(self, version=None, downloader=None, callback=None):

        def on_runner_installed(*args):
            config_path = system.create_folder("~/.vice")
            lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib/vice")
            if not system.path_exists(lib_dir):
                lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib64/vice")
            if not system.path_exists(lib_dir):
                logger.error("Missing lib folder in the Vice runner")
            else:
                system.merge_folders(lib_dir, config_path)
            if callback:
                callback()

        super().install(version, downloader, on_runner_installed)

    def get_roms_path(self, machine=None):
        if not machine:
            machine = "c64"
        paths = {
            "c64": "C64",
            "c128": "C128",
            "vic20": "VIC20",
            "pet": "PET",
            "plus4": "PLUS4",
            "cmbii": "CBM-II",
        }
        root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
        return os.path.join(root_dir, "lib64/vice", paths[machine])

    @staticmethod
    def get_option_prefix(machine):
        prefixes = {
            "c64": "VICII",
            "c128": "VICII",
            "vic20": "VIC",
            "pet": "CRTC",
            "plus4": "TED",
            "cmbii": "CRTC",
        }
        return prefixes[machine]

    @staticmethod
    def get_joydevs(machine):
        joydevs = {"c64": 2, "c128": 2, "vic20": 1, "pet": 0, "plus4": 2, "cmbii": 0}
        return joydevs[machine]

    @staticmethod
    def get_rom_args(machine, rom):
        args = []

        if rom.endswith(".crt"):
            crt_option = {
                "c64": "-cartcrt",
                "c128": "-cartcrt",
                "vic20": "-cartgeneric",
                "pet": None,
                "plus4": "-cart",
                "cmbii": None,
            }
            if crt_option[machine]:
                args.append(crt_option[machine])

        args.append(rom)
        return args

    def play(self):
        machine = self.runner_config.get("machine")

        rom = self.game_config.get("main_file")
        if not rom:
            return {"error": "CUSTOM", "text": "No rom provided"}
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}

        params = [self.get_executable(machine)]
        rom_dir = os.path.dirname(rom)
        params.append("-chdir")
        params.append(rom_dir)
        option_prefix = self.get_option_prefix(machine)

        if self.runner_config.get("fullscreen"):
            params.append("-{}full".format(option_prefix))

        if self.runner_config.get("double"):
            params.append("-{}dsize".format(option_prefix))

        if self.runner_config.get("renderer"):
            params.append("-sdl2renderer")
            params.append(self.runner_config["renderer"])

        if not self.runner_config.get("aspect_ratio", True):
            params.append("-sdlaspectmode")
            params.append("0")

        if self.runner_config.get("drivesound"):
            params.append("-drivesound")

        if self.runner_config.get("joy"):
            for dev in range(self.get_joydevs(machine)):
                params += ["-joydev{}".format(dev + 1), "4"]

        params.extend(self.get_rom_args(machine, rom))
        return {"command": params}
description
game_options
human_name
machine_choices
platforms
runner_options
get_executable(self, machine=None)
Source code in lutris/runners/vice.py
def get_executable(self, machine=None):
    if not machine:
        machine = "c64"
    executables = {
        "c64": "x64",
        "c128": "x128",
        "vic20": "xvic",
        "pet": "xpet",
        "plus4": "xplus4",
        "cbmii": "xcbm2",
    }
    try:
        executable = executables[machine]
    except KeyError as ex:
        raise ValueError("Invalid machine '%s'" % machine) from ex
    return os.path.join(settings.RUNNER_DIR, "vice/bin/%s" % executable)
get_joydevs(machine) staticmethod
Source code in lutris/runners/vice.py
@staticmethod
def get_joydevs(machine):
    joydevs = {"c64": 2, "c128": 2, "vic20": 1, "pet": 0, "plus4": 2, "cmbii": 0}
    return joydevs[machine]
get_option_prefix(machine) staticmethod
Source code in lutris/runners/vice.py
@staticmethod
def get_option_prefix(machine):
    prefixes = {
        "c64": "VICII",
        "c128": "VICII",
        "vic20": "VIC",
        "pet": "CRTC",
        "plus4": "TED",
        "cmbii": "CRTC",
    }
    return prefixes[machine]
get_platform(self)
Source code in lutris/runners/vice.py
def get_platform(self):
    machine = self.game_config.get("machine")
    if machine:
        for index, choice in enumerate(self.machine_choices):
            if choice[1] == machine:
                return self.platforms[index]
    return self.platforms[0]  # Default to C64
get_rom_args(machine, rom) staticmethod
Source code in lutris/runners/vice.py
@staticmethod
def get_rom_args(machine, rom):
    args = []

    if rom.endswith(".crt"):
        crt_option = {
            "c64": "-cartcrt",
            "c128": "-cartcrt",
            "vic20": "-cartgeneric",
            "pet": None,
            "plus4": "-cart",
            "cmbii": None,
        }
        if crt_option[machine]:
            args.append(crt_option[machine])

    args.append(rom)
    return args
get_roms_path(self, machine=None)
Source code in lutris/runners/vice.py
def get_roms_path(self, machine=None):
    if not machine:
        machine = "c64"
    paths = {
        "c64": "C64",
        "c128": "C128",
        "vic20": "VIC20",
        "pet": "PET",
        "plus4": "PLUS4",
        "cmbii": "CBM-II",
    }
    root_dir = os.path.dirname(os.path.dirname(self.get_executable()))
    return os.path.join(root_dir, "lib64/vice", paths[machine])
install(self, version=None, downloader=None, callback=None)

Install runner using package management systems.

Source code in lutris/runners/vice.py
def install(self, version=None, downloader=None, callback=None):

    def on_runner_installed(*args):
        config_path = system.create_folder("~/.vice")
        lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib/vice")
        if not system.path_exists(lib_dir):
            lib_dir = os.path.join(settings.RUNNER_DIR, "vice/lib64/vice")
        if not system.path_exists(lib_dir):
            logger.error("Missing lib folder in the Vice runner")
        else:
            system.merge_folders(lib_dir, config_path)
        if callback:
            callback()

    super().install(version, downloader, on_runner_installed)
play(self)
Source code in lutris/runners/vice.py
def play(self):
    machine = self.runner_config.get("machine")

    rom = self.game_config.get("main_file")
    if not rom:
        return {"error": "CUSTOM", "text": "No rom provided"}
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}

    params = [self.get_executable(machine)]
    rom_dir = os.path.dirname(rom)
    params.append("-chdir")
    params.append(rom_dir)
    option_prefix = self.get_option_prefix(machine)

    if self.runner_config.get("fullscreen"):
        params.append("-{}full".format(option_prefix))

    if self.runner_config.get("double"):
        params.append("-{}dsize".format(option_prefix))

    if self.runner_config.get("renderer"):
        params.append("-sdl2renderer")
        params.append(self.runner_config["renderer"])

    if not self.runner_config.get("aspect_ratio", True):
        params.append("-sdlaspectmode")
        params.append("0")

    if self.runner_config.get("drivesound"):
        params.append("-drivesound")

    if self.runner_config.get("joy"):
        for dev in range(self.get_joydevs(machine)):
            params += ["-joydev{}".format(dev + 1), "4"]

    params.extend(self.get_rom_args(machine, rom))
    return {"command": params}

web

Run web based games

DEFAULT_ICON

web (Runner)

Source code in lutris/runners/web.py
class web(Runner):
    human_name = _("Web")
    description = _("Runs web based games")
    platforms = [_("Web")]
    game_options = [
        {
            "option": "main_file",
            "type": "string",
            "label": _("Full URL or HTML file path"),
            "help": _("The full address of the game's web page or path to a HTML file."),
        }
    ]
    runner_options = [
        {
            "option": "fullscreen",
            "label": _("Open in fullscreen"),
            "type": "bool",
            "default": False,
            "help": _("Launch the game in fullscreen."),
        },
        {
            "option": "maximize_window",
            "label": _("Open window maximized"),
            "type": "bool",
            "default": False,
            "help": _("Maximizes the window when game starts."),
        },
        {
            "option": "window_size",
            "label": _("Window size"),
            "type": "choice_with_entry",
            "choices": [
                "640x480",
                "800x600",
                "1024x768",
                "1280x720",
                "1280x1024",
                "1920x1080",
            ],
            "default": "800x600",
            "help": _("The initial size of the game window when not opened."),
        },
        {
            "option": "disable_resizing",
            "label": _("Disable window resizing (disables fullscreen and maximize)"),
            "type": "bool",
            "default": False,
            "help": _("You can't resize this window."),
        },
        {
            "option": "frameless",
            "label": _("Borderless window"),
            "type": "bool",
            "default": False,
            "help": _("The window has no borders/frame."),
        },
        {
            "option": "disable_menu_bar",
            "label": _("Disable menu bar and default shortcuts"),
            "type": "bool",
            "default": False,
            "help": _("This also disables default keyboard shortcuts, "
                      "like copy/paste and fullscreen toggling."),
        },
        {
            "option": "disable_scrolling",
            "label": _("Disable page scrolling and hide scrollbars"),
            "type": "bool",
            "default": False,
            "help": _("Disables scrolling on the page."),
        },
        {
            "option": "hide_cursor",
            "label": _("Hide mouse cursor"),
            "type": "bool",
            "default": False,
            "help": _("Prevents the mouse cursor from showing "
                      "when hovering above the window."),
        },
        {
            "option":
            "open_links",
            "label":
            _("Open links in game window"),
            "type":
            "bool",
            "default":
            False,
            "help": _(
                "Enable this option if you want clicked links to open inside the "
                "game window. By default all links open in your default web browser."
            ),
        },
        {
            "option": "remove_margin",
            "label": _("Remove default <body> margin & padding"),
            "type": "bool",
            "default": False,
            "help": _("Sets margin and padding to zero "
                      "on &lt;html&gt; and &lt;body&gt; elements."),
        },
        {
            "option": "enable_flash",
            "label": _("Enable Adobe Flash Player"),
            "type": "bool",
            "default": False,
            "help": _("Enable Adobe Flash Player."),
        },
        {
            "option": "user_agent",
            "label": _("Custom User-Agent"),
            "type": "string",
            "default": "",
            "help": _("Overrides the default User-Agent header used by the runner."),
            "advanced": True,
        },
        {
            "option": "devtools",
            "label": _("Debug with Developer Tools"),
            "type": "bool",
            "default": False,
            "help": _("Let's you debug the page."),
            "advanced": True,
        },
        {
            "option": "external_browser",
            "label": _("Open in web browser (old behavior)"),
            "type": "bool",
            "default": False,
            "help": _("Launch the game in a web browser."),
        },
        {
            "option":
            "custom_browser_executable",
            "label":
            _("Custom web browser executable"),
            "type":
            "file",
            "help": _(
                "Select the executable of a browser on your system.\n"
                "If left blank, Lutris will launch your default browser (xdg-open)."
            ),
        },
        {
            "option":
            "custom_browser_args",
            "label":
            _("Web browser arguments"),
            "type":
            "string",
            "default":
            '"$GAME"',
            "help": _(
                "Command line arguments to pass to the executable.\n"
                "$GAME or $URL inserts the game url.\n\n"
                'For Chrome/Chromium app mode use: --app="$GAME"'
            ),
        },
    ]
    system_options_override = [{"option": "disable_runtime", "default": True}]
    runner_executable = "web/electron/electron"

    def get_env(self, os_env=True):
        env = super().get_env(os_env)

        enable_flash_player = self.runner_config.get("enable_flash")
        env["ENABLE_FLASH_PLAYER"] = "1" if enable_flash_player else "0"

        return env

    def play(self):
        url = self.game_config.get("main_file")
        if not url:
            return {
                "error": "CUSTOM",
                "text": _("The web address is empty, \n"
                          "verify the game's configuration."),
            }

        # check if it's an url or a file
        is_url = urlparse(url).scheme != ""

        if not is_url:
            if not system.path_exists(url):
                return {
                    "error": "CUSTOM",
                    "text": _("The file %s does not exist, \n"
                              "verify the game's configuration.") % url,
                }
            url = "file://" + url

        game_data = get_game_by_field(self.config.game_config_id, "configpath")

        # keep the old behavior from browser runner, but with support for extra arguments!
        if self.runner_config.get("external_browser"):
            # is it possible to disable lutris runtime here?
            browser = self.runner_config.get("custom_browser_executable") or "xdg-open"

            args = self.runner_config.get("custom_browser_args")
            args = args or '"$GAME"'
            arguments = string.Template(args).safe_substitute({"GAME": url, "URL": url})

            command = [browser]

            for arg in split_arguments(arguments):
                command.append(arg)

            return {"command": command}

        icon = resources.get_icon_path(game_data.get("slug"))
        if not system.path_exists(icon):
            icon = DEFAULT_ICON

        command = [
            self.get_executable(),
            os.path.join(settings.RUNNER_DIR, "web/electron/resources/app.asar"),
            url,
            "--name",
            game_data.get("name"),
            "--icon",
            icon,
        ]

        for key in [
            "fullscreen",
            "frameless",
            "devtools",
            "disable_resizing",
            "disable_menu_bar",
            "maximize_window",
            "disable_scrolling",
            "hide_cursor",
            "open_links",
            "remove_margin",
        ]:
            if self.runner_config.get(key):
                converted_opt_name = key.replace("_", "-")
                command.append("--{option}".format(option=converted_opt_name))

        if self.runner_config.get("window_size"):
            command.append("--window-size")
            command.append(self.runner_config.get("window_size"))
        if self.runner_config.get("user_agent"):
            command.append("--user-agent")
            command.append(self.runner_config.get("user_agent"))
        if linux.LINUX_SYSTEM.is_flatpak:
            command.append("--no-sandbox")

        return {"command": command, "env": self.get_env(False)}
description
game_options
human_name
platforms
runner_executable
runner_options
system_options_override
get_env(self, os_env=True)

Return environment variables used for a game.

Source code in lutris/runners/web.py
def get_env(self, os_env=True):
    env = super().get_env(os_env)

    enable_flash_player = self.runner_config.get("enable_flash")
    env["ENABLE_FLASH_PLAYER"] = "1" if enable_flash_player else "0"

    return env
play(self)
Source code in lutris/runners/web.py
def play(self):
    url = self.game_config.get("main_file")
    if not url:
        return {
            "error": "CUSTOM",
            "text": _("The web address is empty, \n"
                      "verify the game's configuration."),
        }

    # check if it's an url or a file
    is_url = urlparse(url).scheme != ""

    if not is_url:
        if not system.path_exists(url):
            return {
                "error": "CUSTOM",
                "text": _("The file %s does not exist, \n"
                          "verify the game's configuration.") % url,
            }
        url = "file://" + url

    game_data = get_game_by_field(self.config.game_config_id, "configpath")

    # keep the old behavior from browser runner, but with support for extra arguments!
    if self.runner_config.get("external_browser"):
        # is it possible to disable lutris runtime here?
        browser = self.runner_config.get("custom_browser_executable") or "xdg-open"

        args = self.runner_config.get("custom_browser_args")
        args = args or '"$GAME"'
        arguments = string.Template(args).safe_substitute({"GAME": url, "URL": url})

        command = [browser]

        for arg in split_arguments(arguments):
            command.append(arg)

        return {"command": command}

    icon = resources.get_icon_path(game_data.get("slug"))
    if not system.path_exists(icon):
        icon = DEFAULT_ICON

    command = [
        self.get_executable(),
        os.path.join(settings.RUNNER_DIR, "web/electron/resources/app.asar"),
        url,
        "--name",
        game_data.get("name"),
        "--icon",
        icon,
    ]

    for key in [
        "fullscreen",
        "frameless",
        "devtools",
        "disable_resizing",
        "disable_menu_bar",
        "maximize_window",
        "disable_scrolling",
        "hide_cursor",
        "open_links",
        "remove_margin",
    ]:
        if self.runner_config.get(key):
            converted_opt_name = key.replace("_", "-")
            command.append("--{option}".format(option=converted_opt_name))

    if self.runner_config.get("window_size"):
        command.append("--window-size")
        command.append(self.runner_config.get("window_size"))
    if self.runner_config.get("user_agent"):
        command.append("--user-agent")
        command.append(self.runner_config.get("user_agent"))
    if linux.LINUX_SYSTEM.is_flatpak:
        command.append("--no-sandbox")

    return {"command": command, "env": self.get_env(False)}

wine

Wine runner

DEFAULT_WINE_PREFIX

MIN_SAFE_VERSION

wine (Runner)

Source code in lutris/runners/wine.py
class wine(Runner):
    description = _("Runs Windows games")
    human_name = _("Wine")
    platforms = [_("Windows")]
    multiple_versions = True
    entry_point_option = "exe"

    game_options = [
        {
            "option": "exe",
            "type": "file",
            "label": _("Executable"),
            "help": _("The game's main EXE file"),
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "help": _("Windows command line arguments used when launching the game"),
            "validator": shlex.split
        },
        {
            "option": "working_dir",
            "type": "directory_chooser",
            "label": _("Working directory"),
            "help": _(
                "The location where the game is run from.\n"
                "By default, Lutris uses the directory of the "
                "executable."
            ),
        },
        {
            "option": "prefix",
            "type": "directory_chooser",
            "label": _("Wine prefix"),
            "help": _(
                'The prefix used by Wine.\n'
                "It's a directory containing a set of files and "
                "folders making up a confined Windows environment."
            ),
        },
        {
            "option": "arch",
            "type": "choice",
            "label": _("Prefix architecture"),
            "choices": [(_("Auto"), "auto"), (_("32-bit"), "win32"), (_("64-bit"), "win64")],
            "default": "auto",
            "help": _("The architecture of the Windows environment"),
        },
    ]

    reg_prefix = "HKEY_CURRENT_USER/Software/Wine"
    reg_keys = {
        "Audio": r"%s/Drivers" % reg_prefix,
        "MouseWarpOverride": r"%s/DirectInput" % reg_prefix,
        "Desktop": "MANAGED",
        "WineDesktop": "MANAGED",
        "ShowCrashDialog": "MANAGED"
    }

    core_processes = (
        "services.exe",
        "winedevice.exe",
        "plugplay.exe",
        "explorer.exe",
        "rpcss.exe",
        "rundll32.exe",
        "wineboot.exe",
    )

    def __init__(self, config=None):  # noqa: C901
        super().__init__(config)
        self.dll_overrides = DEFAULT_DLL_OVERRIDES.copy()  # we'll modify this, so we better copy it

        def get_wine_version_choices():
            version_choices = [(_("Custom (select executable below)"), "custom")]
            labels = {
                "winehq-devel": _("WineHQ Devel ({})"),
                "winehq-staging": _("WineHQ Staging ({})"),
                "wine-development": _("Wine Development ({})"),
                "system": _("System ({})"),
            }
            versions = get_wine_versions()
            for version in versions:
                if version in labels:
                    version_number = get_wine_version(WINE_PATHS[version])
                    label = labels[version].format(version_number)
                else:
                    label = version
                version_choices.append((label, version))
            return version_choices

        def esync_limit_callback(widget, option, config):
            limits_set = is_esync_limit_set()
            wine_path = self.get_path_for_version(config["version"])
            wine_ver = is_version_esync(wine_path)
            response = True

            if not wine_ver:
                response = thread_safe_call(esync_display_version_warning)

            if not limits_set:
                thread_safe_call(esync_display_limit_warning)
                response = False

            return widget, option, response

        def fsync_support_callback(widget, option, config):
            fsync_supported = is_fsync_supported()
            wine_path = self.get_path_for_version(config["version"])
            wine_ver = is_version_fsync(wine_path)
            response = True

            if not wine_ver:
                response = thread_safe_call(fsync_display_version_warning)

            if not fsync_supported:
                thread_safe_call(fsync_display_support_warning)
                response = False

            return widget, option, response

        def dxvk_vulkan_callback(widget, option, config):
            response = True
            if not is_vulkan_supported():
                if not thread_safe_call(display_vulkan_error):
                    response = False
            return widget, option, response

        self.runner_options = [
            {
                "option": "version",
                "label": _("Wine version"),
                "type": "choice",
                "choices": get_wine_version_choices,
                "default": get_default_version(),
                "help": _(
                    "The version of Wine used to launch the game.\n"
                    "Using the last version is generally recommended, "
                    "but some games work better on older versions."
                ),
            },
            {
                "option": "custom_wine_path",
                "label": _("Custom Wine executable"),
                "type": "file",
                "advanced": True,
                "help": _("The Wine executable to be used if you have "
                          'selected "Custom" as the Wine version.'),
            },
            {
                "option": "system_winetricks",
                "label": _("Use system winetricks"),
                "type": "bool",
                "default": False,
                "advanced": True,
                "help": _("Switch on to use /usr/bin/winetricks for winetricks."),
            },
            {
                "option": "dxvk",
                "label": _("Enable DXVK"),
                "type": "extended_bool",
                "callback": dxvk_vulkan_callback,
                "callback_on": True,
                "default": True,
                "active": True,
                "help": _(
                    "Use DXVK to "
                    "increase compatibility and performance in Direct3D 11, 10 "
                    "and 9 applications by translating their calls to Vulkan."),
            },
            {
                "option": "dxvk_version",
                "label": _("DXVK version"),
                "advanced": True,
                "type": "choice_with_entry",
                "choices": DXVKManager().version_choices,
                "default": DXVKManager().version,
            },

            {
                "option": "vkd3d",
                "label": _("Enable VKD3D"),
                "type": "extended_bool",
                "callback": dxvk_vulkan_callback,
                "callback_on": True,
                "default": True,
                "active": True,
                "help": _(
                    "Use VKD3D to enable support for Direct3D 12 "
                    "applications by translating their calls to Vulkan."),
            },
            {
                "option": "vkd3d_version",
                "label": _("VKD3D version"),
                "advanced": True,
                "type": "choice_with_entry",
                "choices": VKD3DManager().version_choices,
                "default": VKD3DManager().version,
            },
            {
                "option": "d3d_extras",
                "label": _("Enable D3D Extras"),
                "type": "bool",
                "default": True,
                "advanced": True,
                "help": _(
                    "Replace Wine's D3DX and D3DCOMPILER libraries with alternative ones. "
                    "Needed for proper functionality of DXVK with some games."
                ),
            },
            {
                "option": "d3d_extras_version",
                "label": _("D3D Extras version"),
                "advanced": True,
                "type": "choice_with_entry",
                "choices": D3DExtrasManager().version_choices,
                "default": D3DExtrasManager().version,
            },
            {
                "option": "dxvk_nvapi",
                "label": _("Enable DXVK-NVAPI / DLSS"),
                "type": "bool",
                "default": True,
                "advanced": True,
                "help": _(
                    "Enable emulation of Nvidia's NVAPI and add DLSS support, if available."
                ),
            },
            {
                "option": "dxvk_nvapi_version",
                "label": _("DXVK NVAPI version"),
                "advanced": True,
                "type": "choice_with_entry",
                "choices": DXVKNVAPIManager().version_choices,
                "default": DXVKNVAPIManager().version,
            },
            {
                "option": "dgvoodoo2",
                "label": _("Enable dgvoodoo2"),
                "type": "bool",
                "default": False,
                "advanced": False,
                "help": _(
                    "dgvoodoo2 is an alternative translation layer for rendering old games "
                    "that utilize D3D1-7 and Glide APIs. As it translates to D3D11, it's "
                    "recommended to use it in combination with DXVK. Only 32-bit apps are supported."
                ),
            },
            {
                "option": "dgvoodoo2_version",
                "label": _("dgvoodoo2 version"),
                "advanced": True,
                "type": "choice_with_entry",
                "choices": dgvoodoo2Manager().version_choices,
                "default": dgvoodoo2Manager().version,
            },
            {
                "option": "esync",
                "label": _("Enable Esync"),
                "type": "extended_bool",
                "callback": esync_limit_callback,
                "callback_on": True,
                "active": True,
                "default": True,
                "help": _(
                    "Enable eventfd-based synchronization (esync). "
                    "This will increase performance in applications "
                    "that take advantage of multi-core processors."
                ),
            },
            {
                "option": "fsync",
                "label": _("Enable Fsync"),
                "type": "extended_bool",
                "default": True,
                "callback": fsync_support_callback,
                "callback_on": True,
                "active": True,
                "help": _(
                    "Enable futex-based synchronization (fsync). "
                    "This will increase performance in applications "
                    "that take advantage of multi-core processors. "
                    "Requires a custom kernel with the fsync patchset."
                ),
            },
            {
                "option": "fsr",
                "label": _("Enable AMD FidelityFX Super Resolution (FSR)"),
                "type": "bool",
                "default": True,
                "help": _(
                    "Use FSR to upscale the game window to native resolution.\n"
                    "Requires Lutris Wine FShack >= 6.13 and setting the game to a lower resolution.\n"
                    "Does not work with games running in borderless window mode or that perform their own upscaling."
                ),
            },
            {
                "option": "battleye",
                "label": _("Enable BattlEye Anti-Cheat"),
                "type": "bool",
                "default": False,
                "help": _(
                    "Enable support for BattlEye Anti-Cheat in supported games\n"
                    "Requires Lutris Wine 6.21-2 and newer or any other compatible Wine build.\n"
                ),
            },
            {
                "option": "Desktop",
                "label": _("Windowed (virtual desktop)"),
                "type": "bool",
                "default": False,
                "help": _(
                    "Run the whole Windows desktop in a window.\n"
                    "Otherwise, run it fullscreen.\n"
                    "This corresponds to Wine's Virtual Desktop option."
                ),
            },
            {
                "option": "WineDesktop",
                "label": _("Virtual desktop resolution"),
                "type": "choice_with_entry",
                "choices": DISPLAY_MANAGER.get_resolutions,
                "help": _("The size of the virtual desktop in pixels."),
            },
            {
                "option": "Dpi",
                "label": _("Enable DPI Scaling"),
                "type": "bool",
                "default": False,
                "help": _(
                    "Enables the Windows application's DPI scaling.\n"
                    "Otherwise, disables DPI scaling by using 96 DPI.\n"
                    "This corresponds to Wine's Screen Resolution option."
                ),
            },
            {
                "option": "ExplicitDpi",
                "label": _("DPI"),
                "type": "string",
                "help": _(
                    "The DPI to be used if 'Enable DPI Scaling' is turned on.\n"
                    "If blank or 'auto', Lutris will auto-detect this."
                ),
            },
            {
                "option": "MouseWarpOverride",
                "label": _("Mouse Warp Override"),
                "type": "choice",
                "choices": [
                    (_("Enable"), "enable"),
                    (_("Disable"), "disable"),
                    (_("Force"), "force"),
                ],
                "default": "enable",
                "advanced": True,
                "help": _(
                    "Override the default mouse pointer warping behavior\n"
                    "<b>Enable</b>: (Wine default) warp the pointer when the "
                    "mouse is exclusively acquired \n"
                    "<b>Disable</b>: never warp the mouse pointer \n"
                    "<b>Force</b>: always warp the pointer"
                ),
            },
            {
                "option": "Audio",
                "label": _("Audio driver"),
                "type": "choice",
                "advanced": True,
                "choices": [
                    (_("Auto"), "auto"),
                    ("ALSA", "alsa"),
                    ("PulseAudio", "pulse"),
                    ("OSS", "oss"),
                ],
                "default": "auto",
                "help": _(
                    "Which audio backend to use.\n"
                    "By default, Wine automatically picks the right one "
                    "for your system."
                ),
            },
            {
                "option": "overrides",
                "type": "mapping",
                "label": _("DLL overrides"),
                "help": _("Sets WINEDLLOVERRIDES when launching the game."),
            },
            {
                "option": "show_debug",
                "label": _("Output debugging info"),
                "type": "choice",
                "choices": [
                    (_("Disabled"), "-all"),
                    (_("Enabled"), ""),
                    (_("Inherit from environment"), "inherit"),
                    (_("Show FPS"), "+fps"),
                    (_("Full (CAUTION: Will cause MASSIVE slowdown)"), "+all"),
                ],
                "default": "-all",
                "help": _("Output debugging information in the game log "
                          "(might affect performance)"),
            },
            {
                "option": "ShowCrashDialog",
                "label": _("Show crash dialogs"),
                "type": "bool",
                "default": False,
                "advanced": True,
            },
            {
                "option": "autoconf_joypad",
                "type": "bool",
                "label": _("Autoconfigure joypads"),
                "advanced": True,
                "default": False,
                "help":
                _("Automatically disables one of Wine's detected joypad "
                  "to avoid having 2 controllers detected"),
            },
            {
                "option": "sandbox",
                "type": "bool",
                "label": _("Create a sandbox for Wine folders"),
                "default": True,
                "advanced": True,
                "help": _(
                    "Do not use $HOME for desktop integration folders.\n"
                    "By default, it use the directories in the confined "
                    "Windows environment."
                ),
            },
            {
                "option": "sandbox_dir",
                "type": "directory_chooser",
                "label": _("Sandbox directory"),
                "help": _("Custom directory for desktop integration folders."),
                "advanced": True,
            },
        ]

    @property
    def context_menu_entries(self):
        """Return the contexual menu entries for wine"""
        menu_entries = [("wineexec", _("Run EXE inside Wine prefix"), self.run_wineexec)]
        if "Proton" not in self.get_version():
            menu_entries.append(("winecfg", _("Wine configuration"), self.run_winecfg))
        menu_entries += [
            ("wineshell", _("Open Bash terminal"), self.run_wine_terminal),
            ("wineconsole", _("Open Wine console"), self.run_wineconsole),
            ("wine-regedit", _("Wine registry"), self.run_regedit),
            ("winetricks", _("Winetricks"), self.run_winetricks),
            ("winecpl", _("Wine Control Panel"), self.run_winecpl),
        ]
        return menu_entries

    @property
    def prefix_path(self):
        """Return the absolute path of the Wine prefix"""
        _prefix_path = self.game_config.get("prefix") \
            or os.environ.get("WINEPREFIX")
        if not _prefix_path and self.game_config.get("exe"):
            # Find prefix from game if we have one
            _prefix_path = find_prefix(self.game_exe)
        if not _prefix_path:
            _prefix_path = DEFAULT_WINE_PREFIX
        return os.path.expanduser(_prefix_path)

    @property
    def game_exe(self):
        """Return the game's executable's path, which may not exist. None
        if there is no exe path defined."""
        exe = self.game_config.get("exe")
        if not exe:
            logger.error("The game doesn't have an executable")
            return None
        if os.path.isabs(exe):
            return system.fix_path_case(exe)
        if not self.game_path:
            logger.warning("The game has an executable, but not a game path")
            return None
        return system.fix_path_case(os.path.join(self.game_path, exe))

    @property
    def working_dir(self):
        """Return the working directory to use when running the game."""
        option = self.game_config.get("working_dir")
        if option:
            return option
        if self.game_exe:
            game_dir = os.path.dirname(self.game_exe)
            if os.path.isdir(game_dir):
                return game_dir
        return super().working_dir

    @property
    def nvidia_shader_cache_path(self):
        """WINE should give each game its own shader cache if possible."""
        return self.game_path or self.shader_cache_dir

    @property
    def wine_arch(self):
        """Return the wine architecture.

        Get it from the config or detect it from the prefix"""
        arch = self.game_config.get("arch") or "auto"
        if arch not in ("win32", "win64"):
            arch = detect_arch(self.prefix_path, self.get_executable())
        return arch

    def get_version(self, use_default=True):
        """Return the Wine version to use. use_default can be set to false to
        force the installation of a specific wine version"""
        runner_version = self.runner_config.get("version")
        if runner_version:
            return runner_version
        if use_default:
            return get_default_version()

    def get_path_for_version(self, version):
        """Return the absolute path of a wine executable for a given version"""
        if version in WINE_PATHS:
            return system.find_executable(WINE_PATHS[version])
        if "Proton" in version:
            for proton_path in get_proton_paths():
                if os.path.isfile(os.path.join(proton_path, version, "dist/bin/wine")):
                    return os.path.join(proton_path, version, "dist/bin/wine")
        if version.startswith("PlayOnLinux"):
            version, arch = version.split()[1].rsplit("-", 1)
            return os.path.join(POL_PATH, "wine", "linux-" + arch, version, "bin/wine")
        if version == "custom":
            return self.runner_config.get("custom_wine_path", "")
        return os.path.join(WINE_DIR, version, "bin/wine")

    def get_executable(self, version=None, fallback=True):
        """Return the path to the Wine executable.
        A specific version can be specified if needed.
        """
        if version is None:
            version = self.get_version()
        if not version:
            return

        wine_path = self.get_path_for_version(version)
        if system.path_exists(wine_path):
            return wine_path

        if fallback:
            # Fallback to default version
            default_version = get_default_version()
            wine_path = self.get_path_for_version(default_version)
            if wine_path:
                # Update the version in the config
                if version == self.runner_config.get("version"):
                    self.runner_config["version"] = default_version
                    # TODO: runner_config is a dict so we have to instanciate a
                    # LutrisConfig object to save it.
                    # XXX: The version key could be either in the game specific
                    # config or the runner specific config. We need to know
                    # which one to get the correct LutrisConfig object.
            return wine_path

    def is_installed(self, version=None, fallback=True, min_version=None):
        """Check if Wine is installed.
        If no version is passed, checks if any version of wine is available
        """
        if version:
            return system.path_exists(self.get_executable(version, fallback))

        wine_versions = get_wine_versions()
        if min_version:
            min_version_list, _, _ = parse_version(min_version)
            for wine_version in wine_versions:
                version_list, _, _ = parse_version(wine_version)
                if version_list > min_version_list:
                    return True
            logger.warning("Wine %s or higher not found", min_version)
        return bool(wine_versions)

    @classmethod
    def msi_exec(
        cls,
        msi_file,
        quiet=False,
        prefix=None,
        wine_path=None,
        working_dir=None,
        blocking=False,
    ):
        msi_args = "/i %s" % msi_file
        if quiet:
            msi_args += " /q"
        return wineexec(
            "msiexec",
            args=msi_args,
            prefix=prefix,
            wine_path=wine_path,
            working_dir=working_dir,
            blocking=blocking,
        )

    def _run_executable(self, executable):
        """Runs a Windows executable using this game's configuration"""
        wineexec(
            executable,
            wine_path=self.get_executable(),
            prefix=self.prefix_path,
            working_dir=self.prefix_path,
            config=self,
            env=self.get_env(os_env=True),
        )

    def run_wineexec(self, *args):
        """Ask the user for an arbitrary exe file to run in the game's prefix"""
        dlg = FileDialog(_("Select an EXE or MSI file"), default_path=self.game_path)
        filename = dlg.filename
        if not filename:
            return
        self.prelaunch()
        self._run_executable(filename)

    def run_wineconsole(self, *args):
        """Runs wineconsole inside wine prefix."""
        self.prelaunch()
        self._run_executable("wineconsole")

    def run_winecfg(self, *args):
        """Run winecfg in the current context"""
        self.prelaunch()
        winecfg(
            wine_path=self.get_executable(),
            prefix=self.prefix_path,
            arch=self.wine_arch,
            config=self,
            env=self.get_env(os_env=True),
        )

    def run_regedit(self, *args):
        """Run regedit in the current context"""
        self.prelaunch()
        self._run_executable("regedit")

    def run_wine_terminal(self, *args):
        terminal = self.system_config.get("terminal_app")
        open_wine_terminal(
            terminal=terminal,
            wine_path=self.get_executable(),
            prefix=self.prefix_path,
            env=self.get_env(os_env=True)
        )

    def run_winetricks(self, *args):
        """Run winetricks in the current context"""
        self.prelaunch()
        winetricks(
            "", prefix=self.prefix_path, wine_path=self.get_executable(), config=self, env=self.get_env(os_env=True)
        )

    def run_winecpl(self, *args):
        """Execute Wine control panel."""
        self.prelaunch()
        self._run_executable("control")

    def run_winekill(self, *args):
        """Runs wineserver -k."""
        winekill(
            self.prefix_path,
            arch=self.wine_arch,
            wine_path=self.get_executable(),
            env=self.get_env(),
            initial_pids=self.get_pids(),
        )
        return True

    def set_regedit_keys(self):
        """Reset regedit keys according to config."""
        prefix_manager = WinePrefixManager(self.prefix_path)
        # Those options are directly changed with the prefix manager and skip
        # any calls to regedit.
        managed_keys = {
            "ShowCrashDialog": prefix_manager.set_crash_dialogs,
            "Desktop": prefix_manager.set_virtual_desktop,
            "WineDesktop": prefix_manager.set_desktop_size,
        }

        for key, path in self.reg_keys.items():
            value = self.runner_config.get(key) or "auto"
            if not value or value == "auto" and key not in managed_keys:
                prefix_manager.clear_registry_subkeys(path, key)
            elif key in self.runner_config:
                if key in managed_keys:
                    # Do not pass fallback 'auto' value to managed keys
                    if value == "auto":
                        value = None
                    managed_keys[key](value)
                    continue
                # Convert numeric strings to integers so they are saved as dword
                if value.isdigit():
                    value = int(value)

                prefix_manager.set_registry_key(path, key, value)

        # We always configure the DPI, because if the user turns off DPI scaling, but it
        # had been on the only way to implement that is to save 96 DPI into the registry.
        prefix_manager.set_dpi(self.get_dpi())

    def get_dpi(self):
        """Return the DPI to be used by Wine; returns 96 to disable scaling,
        as this is Window's unscaled default DPI."""
        if bool(self.runner_config.get("Dpi")):
            explicit_dpi = self.runner_config.get("ExplicitDpi")
            if explicit_dpi == "auto":
                explicit_dpi = None
            try:
                explicit_dpi = int(explicit_dpi)
            except ValueError:
                explicit_dpi = None
            return explicit_dpi or get_default_dpi()

        return 96

    def setup_dlls(self, manager_class, enable, version):
        """Enable or disable DLLs"""
        dll_manager = manager_class(
            self.prefix_path,
            arch=self.wine_arch,
            version=version,
        )
        # manual version only sets the dlls to native
        if dll_manager.version.lower() != "manual":
            if enable:
                dll_manager.enable()
            else:
                dll_manager.disable()

        if enable:
            for dll in dll_manager.managed_dlls:
                # We have to make sure that the dll exists before setting it to native
                if dll_manager.dll_exists(dll):
                    self.dll_overrides[dll] = "n"

    def prelaunch(self):
        if not system.path_exists(os.path.join(self.prefix_path, "user.reg")):
            logger.warning("No valid prefix detected in %s, creating one...", self.prefix_path)
            create_prefix(self.prefix_path, wine_path=self.get_executable(), arch=self.wine_arch)

        prefix_manager = WinePrefixManager(self.prefix_path)
        if self.runner_config.get("autoconf_joypad", False):
            prefix_manager.configure_joypads()
        self.sandbox(prefix_manager)
        self.set_regedit_keys()

        self.setup_dlls(
            DXVKManager,
            bool(self.runner_config.get("dxvk")),
            self.runner_config.get("dxvk_version")
        )
        self.setup_dlls(
            VKD3DManager,
            bool(self.runner_config.get("vkd3d")),
            self.runner_config.get("vkd3d_version")
        )
        self.setup_dlls(
            DXVKNVAPIManager,
            bool(self.runner_config.get("dxvk_nvapi")),
            self.runner_config.get("dxvk_nvapi_version")
        )
        self.setup_dlls(
            D3DExtrasManager,
            bool(self.runner_config.get("d3d_extras")),
            self.runner_config.get("d3d_extras_version")
        )
        self.setup_dlls(
            dgvoodoo2Manager,
            bool(self.runner_config.get("dgvoodoo2")),
            self.runner_config.get("dgvoodoo2_version")
        )
        return True

    def get_dll_overrides(self):
        """Return the DLLs overriden at runtime"""
        try:
            overrides = self.runner_config["overrides"]
        except KeyError:
            overrides = {}
        if not isinstance(overrides, dict):
            logger.warning("DLL overrides is not a mapping: %s", overrides)
            overrides = {}
        return overrides

    def get_env(self, os_env=False):
        """Return environment variables used by the game"""
        # Always false to runner.get_env, the default value
        # of os_env is inverted in the wine class,
        # the OS env is read later.
        env = super().get_env(False)
        if os_env:
            env.update(os.environ.copy())
        show_debug = self.runner_config.get("show_debug", "-all")
        if show_debug != "inherit":
            env["WINEDEBUG"] = show_debug
        if show_debug == "-all":
            env["DXVK_LOG_LEVEL"] = "none"
        env["WINEARCH"] = self.wine_arch
        env["WINE"] = self.get_executable()
        env["WINE_MONO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "mono")
        env["WINE_GECKO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "gecko")
        if is_gstreamer_build(self.get_executable()):
            path_64 = os.path.join(WINE_DIR, self.get_version(), "lib64/gstreamer-1.0/")
            path_32 = os.path.join(WINE_DIR, self.get_version(), "lib/gstreamer-1.0/")
            if os.path.exists(path_64) or os.path.exists(path_32):
                env["GST_PLUGIN_SYSTEM_PATH_1_0"] = path_64 + ":" + path_32
        if self.prefix_path:
            env["WINEPREFIX"] = self.prefix_path

        if not ("WINEESYNC" in env and env["WINEESYNC"] == "1"):
            env["WINEESYNC"] = "1" if self.runner_config.get("esync") else "0"

        if not ("WINEFSYNC" in env and env["WINEFSYNC"] == "1"):
            env["WINEFSYNC"] = "1" if self.runner_config.get("fsync") else "0"

        if self.runner_config.get("fsr"):
            env["WINE_FULLSCREEN_FSR"] = "1"

        if self.runner_config.get("dxvk_nvapi"):
            env["DXVK_NVAPIHACK"] = "0"

        if self.runner_config.get("battleye"):
            env["PROTON_BATTLEYE_RUNTIME"] = os.path.join(settings.RUNTIME_DIR, "battleye_runtime")

        overrides = self.get_dll_overrides()
        if overrides:
            self.dll_overrides.update(overrides)
        env["WINEDLLOVERRIDES"] = get_overrides_env(self.dll_overrides)
        return env

    def get_runtime_env(self):
        """Return runtime environment variables with path to wine for Lutris builds"""
        wine_path = self.get_executable()
        wine_root = None
        if WINE_DIR:
            wine_root = os.path.dirname(os.path.dirname(wine_path))
        for proton_path in get_proton_paths():
            if proton_path in wine_path:
                wine_root = os.path.dirname(os.path.dirname(wine_path))
        return runtime.get_env(
            version="Ubuntu-18.04",
            prefer_system_libs=self.system_config.get("prefer_system_libs", True),
            wine_path=wine_root,
        )

    def get_pids(self, wine_path=None):
        """Return a list of pids of processes using the current wine exe."""
        if wine_path:
            exe = wine_path
        else:
            exe = self.get_executable()
        if not exe.startswith("/"):
            exe = system.find_executable(exe)
        pids = system.get_pids_using_file(exe)
        if self.wine_arch == "win64" and os.path.basename(exe) == "wine":
            pids = pids | system.get_pids_using_file(exe + "64")

        # Add wineserver PIDs to the mix (at least one occurence of fuser not
        # picking the games's PID from wine/wine64 but from wineserver for some
        # unknown reason.
        pids = pids | system.get_pids_using_file(os.path.join(os.path.dirname(exe), "wineserver"))
        return pids

    def sandbox(self, wine_prefix):
        if self.runner_config.get("sandbox", True):
            wine_prefix.desktop_integration(desktop_dir=self.runner_config.get("sandbox_dir"))
        else:
            wine_prefix.desktop_integration(restore=True)

    def play(self):  # pylint: disable=too-many-return-statements # noqa: C901
        game_exe = self.game_exe
        arguments = self.game_config.get("args", "")
        launch_info = {"env": self.get_env(os_env=False)}
        using_dxvk = self.runner_config.get("dxvk")

        if using_dxvk:
            # Set this to 1 to enable access to more RAM for 32bit applications
            launch_info["env"]["WINE_LARGE_ADDRESS_AWARE"] = "1"
            if not is_vulkan_supported():
                if not display_vulkan_error(True):
                    return {"error": "VULKAN_NOT_FOUND"}

        if not game_exe or not system.path_exists(game_exe):
            return {"error": "FILE_NOT_FOUND", "file": game_exe}

        if launch_info["env"].get("WINEESYNC") == "1":
            limit_set = is_esync_limit_set()
            wine_ver = is_version_esync(self.get_executable())

            if not limit_set and not wine_ver:
                esync_display_version_warning(True)
                esync_display_limit_warning()
                return {"error": "ESYNC_LIMIT_NOT_SET"}
            if not is_esync_limit_set():
                esync_display_limit_warning()
                return {"error": "ESYNC_LIMIT_NOT_SET"}
            if not wine_ver:
                if not esync_display_version_warning(True):
                    return {"error": "NON_ESYNC_WINE_VERSION"}

        if launch_info["env"].get("WINEFSYNC") == "1":
            fsync_supported = is_fsync_supported()
            wine_ver = is_version_fsync(self.get_executable())

            if not fsync_supported and not wine_ver:
                fsync_display_version_warning(True)
                fsync_display_support_warning()
                return {"error": "FSYNC_NOT_SUPPORTED"}
            if not fsync_supported:
                fsync_display_support_warning()
                return {"error": "FSYNC_NOT_SUPPORTED"}
            if not wine_ver:
                if not fsync_display_version_warning(True):
                    return {"error": "NON_FSYNC_WINE_VERSION"}

        command = [self.get_executable()]

        game_exe, args, _working_dir = get_real_executable(game_exe, self.working_dir)
        command.append(game_exe)
        if args:
            command = command + args

        if arguments:
            for arg in split_arguments(arguments):
                command.append(arg)
        launch_info["command"] = command
        return launch_info

    def force_stop_game(self, game):
        """Kill WINE with kindness, or at least with -k. This seems to leave a process
        alive for some reason, but the caller will detect this and SIGKILL it."""
        self.run_winekill()

    @staticmethod
    def parse_wine_path(path, prefix_path=None):
        """Take a Windows path, return the corresponding Linux path."""
        if not prefix_path:
            prefix_path = os.path.expanduser("~/.wine")

        path = path.replace("\\\\", "/").replace("\\", "/")

        if path[1] == ":":  # absolute path
            drive = os.path.join(prefix_path, "dosdevices", path[:2].lower())
            if os.path.islink(drive):  # Try to resolve the path
                drive = os.readlink(drive)
            return os.path.join(drive, path[3:])

        if path[0] == "/":  # drive-relative path. C is as good a guess as any..
            return os.path.join(prefix_path, "drive_c", path[1:])

        # Relative path
        return path
context_menu_entries property readonly

Return the contexual menu entries for wine

core_processes
description
entry_point_option
game_exe property readonly

Return the game's executable's path, which may not exist. None if there is no exe path defined.

game_options
human_name
multiple_versions
nvidia_shader_cache_path property readonly

WINE should give each game its own shader cache if possible.

platforms
prefix_path property readonly

Return the absolute path of the Wine prefix

reg_keys
reg_prefix
wine_arch property readonly

Return the wine architecture.

Get it from the config or detect it from the prefix

working_dir property readonly

Return the working directory to use when running the game.

__init__(self, config=None) special
Source code in lutris/runners/wine.py
def __init__(self, config=None):  # noqa: C901
    super().__init__(config)
    self.dll_overrides = DEFAULT_DLL_OVERRIDES.copy()  # we'll modify this, so we better copy it

    def get_wine_version_choices():
        version_choices = [(_("Custom (select executable below)"), "custom")]
        labels = {
            "winehq-devel": _("WineHQ Devel ({})"),
            "winehq-staging": _("WineHQ Staging ({})"),
            "wine-development": _("Wine Development ({})"),
            "system": _("System ({})"),
        }
        versions = get_wine_versions()
        for version in versions:
            if version in labels:
                version_number = get_wine_version(WINE_PATHS[version])
                label = labels[version].format(version_number)
            else:
                label = version
            version_choices.append((label, version))
        return version_choices

    def esync_limit_callback(widget, option, config):
        limits_set = is_esync_limit_set()
        wine_path = self.get_path_for_version(config["version"])
        wine_ver = is_version_esync(wine_path)
        response = True

        if not wine_ver:
            response = thread_safe_call(esync_display_version_warning)

        if not limits_set:
            thread_safe_call(esync_display_limit_warning)
            response = False

        return widget, option, response

    def fsync_support_callback(widget, option, config):
        fsync_supported = is_fsync_supported()
        wine_path = self.get_path_for_version(config["version"])
        wine_ver = is_version_fsync(wine_path)
        response = True

        if not wine_ver:
            response = thread_safe_call(fsync_display_version_warning)

        if not fsync_supported:
            thread_safe_call(fsync_display_support_warning)
            response = False

        return widget, option, response

    def dxvk_vulkan_callback(widget, option, config):
        response = True
        if not is_vulkan_supported():
            if not thread_safe_call(display_vulkan_error):
                response = False
        return widget, option, response

    self.runner_options = [
        {
            "option": "version",
            "label": _("Wine version"),
            "type": "choice",
            "choices": get_wine_version_choices,
            "default": get_default_version(),
            "help": _(
                "The version of Wine used to launch the game.\n"
                "Using the last version is generally recommended, "
                "but some games work better on older versions."
            ),
        },
        {
            "option": "custom_wine_path",
            "label": _("Custom Wine executable"),
            "type": "file",
            "advanced": True,
            "help": _("The Wine executable to be used if you have "
                      'selected "Custom" as the Wine version.'),
        },
        {
            "option": "system_winetricks",
            "label": _("Use system winetricks"),
            "type": "bool",
            "default": False,
            "advanced": True,
            "help": _("Switch on to use /usr/bin/winetricks for winetricks."),
        },
        {
            "option": "dxvk",
            "label": _("Enable DXVK"),
            "type": "extended_bool",
            "callback": dxvk_vulkan_callback,
            "callback_on": True,
            "default": True,
            "active": True,
            "help": _(
                "Use DXVK to "
                "increase compatibility and performance in Direct3D 11, 10 "
                "and 9 applications by translating their calls to Vulkan."),
        },
        {
            "option": "dxvk_version",
            "label": _("DXVK version"),
            "advanced": True,
            "type": "choice_with_entry",
            "choices": DXVKManager().version_choices,
            "default": DXVKManager().version,
        },

        {
            "option": "vkd3d",
            "label": _("Enable VKD3D"),
            "type": "extended_bool",
            "callback": dxvk_vulkan_callback,
            "callback_on": True,
            "default": True,
            "active": True,
            "help": _(
                "Use VKD3D to enable support for Direct3D 12 "
                "applications by translating their calls to Vulkan."),
        },
        {
            "option": "vkd3d_version",
            "label": _("VKD3D version"),
            "advanced": True,
            "type": "choice_with_entry",
            "choices": VKD3DManager().version_choices,
            "default": VKD3DManager().version,
        },
        {
            "option": "d3d_extras",
            "label": _("Enable D3D Extras"),
            "type": "bool",
            "default": True,
            "advanced": True,
            "help": _(
                "Replace Wine's D3DX and D3DCOMPILER libraries with alternative ones. "
                "Needed for proper functionality of DXVK with some games."
            ),
        },
        {
            "option": "d3d_extras_version",
            "label": _("D3D Extras version"),
            "advanced": True,
            "type": "choice_with_entry",
            "choices": D3DExtrasManager().version_choices,
            "default": D3DExtrasManager().version,
        },
        {
            "option": "dxvk_nvapi",
            "label": _("Enable DXVK-NVAPI / DLSS"),
            "type": "bool",
            "default": True,
            "advanced": True,
            "help": _(
                "Enable emulation of Nvidia's NVAPI and add DLSS support, if available."
            ),
        },
        {
            "option": "dxvk_nvapi_version",
            "label": _("DXVK NVAPI version"),
            "advanced": True,
            "type": "choice_with_entry",
            "choices": DXVKNVAPIManager().version_choices,
            "default": DXVKNVAPIManager().version,
        },
        {
            "option": "dgvoodoo2",
            "label": _("Enable dgvoodoo2"),
            "type": "bool",
            "default": False,
            "advanced": False,
            "help": _(
                "dgvoodoo2 is an alternative translation layer for rendering old games "
                "that utilize D3D1-7 and Glide APIs. As it translates to D3D11, it's "
                "recommended to use it in combination with DXVK. Only 32-bit apps are supported."
            ),
        },
        {
            "option": "dgvoodoo2_version",
            "label": _("dgvoodoo2 version"),
            "advanced": True,
            "type": "choice_with_entry",
            "choices": dgvoodoo2Manager().version_choices,
            "default": dgvoodoo2Manager().version,
        },
        {
            "option": "esync",
            "label": _("Enable Esync"),
            "type": "extended_bool",
            "callback": esync_limit_callback,
            "callback_on": True,
            "active": True,
            "default": True,
            "help": _(
                "Enable eventfd-based synchronization (esync). "
                "This will increase performance in applications "
                "that take advantage of multi-core processors."
            ),
        },
        {
            "option": "fsync",
            "label": _("Enable Fsync"),
            "type": "extended_bool",
            "default": True,
            "callback": fsync_support_callback,
            "callback_on": True,
            "active": True,
            "help": _(
                "Enable futex-based synchronization (fsync). "
                "This will increase performance in applications "
                "that take advantage of multi-core processors. "
                "Requires a custom kernel with the fsync patchset."
            ),
        },
        {
            "option": "fsr",
            "label": _("Enable AMD FidelityFX Super Resolution (FSR)"),
            "type": "bool",
            "default": True,
            "help": _(
                "Use FSR to upscale the game window to native resolution.\n"
                "Requires Lutris Wine FShack >= 6.13 and setting the game to a lower resolution.\n"
                "Does not work with games running in borderless window mode or that perform their own upscaling."
            ),
        },
        {
            "option": "battleye",
            "label": _("Enable BattlEye Anti-Cheat"),
            "type": "bool",
            "default": False,
            "help": _(
                "Enable support for BattlEye Anti-Cheat in supported games\n"
                "Requires Lutris Wine 6.21-2 and newer or any other compatible Wine build.\n"
            ),
        },
        {
            "option": "Desktop",
            "label": _("Windowed (virtual desktop)"),
            "type": "bool",
            "default": False,
            "help": _(
                "Run the whole Windows desktop in a window.\n"
                "Otherwise, run it fullscreen.\n"
                "This corresponds to Wine's Virtual Desktop option."
            ),
        },
        {
            "option": "WineDesktop",
            "label": _("Virtual desktop resolution"),
            "type": "choice_with_entry",
            "choices": DISPLAY_MANAGER.get_resolutions,
            "help": _("The size of the virtual desktop in pixels."),
        },
        {
            "option": "Dpi",
            "label": _("Enable DPI Scaling"),
            "type": "bool",
            "default": False,
            "help": _(
                "Enables the Windows application's DPI scaling.\n"
                "Otherwise, disables DPI scaling by using 96 DPI.\n"
                "This corresponds to Wine's Screen Resolution option."
            ),
        },
        {
            "option": "ExplicitDpi",
            "label": _("DPI"),
            "type": "string",
            "help": _(
                "The DPI to be used if 'Enable DPI Scaling' is turned on.\n"
                "If blank or 'auto', Lutris will auto-detect this."
            ),
        },
        {
            "option": "MouseWarpOverride",
            "label": _("Mouse Warp Override"),
            "type": "choice",
            "choices": [
                (_("Enable"), "enable"),
                (_("Disable"), "disable"),
                (_("Force"), "force"),
            ],
            "default": "enable",
            "advanced": True,
            "help": _(
                "Override the default mouse pointer warping behavior\n"
                "<b>Enable</b>: (Wine default) warp the pointer when the "
                "mouse is exclusively acquired \n"
                "<b>Disable</b>: never warp the mouse pointer \n"
                "<b>Force</b>: always warp the pointer"
            ),
        },
        {
            "option": "Audio",
            "label": _("Audio driver"),
            "type": "choice",
            "advanced": True,
            "choices": [
                (_("Auto"), "auto"),
                ("ALSA", "alsa"),
                ("PulseAudio", "pulse"),
                ("OSS", "oss"),
            ],
            "default": "auto",
            "help": _(
                "Which audio backend to use.\n"
                "By default, Wine automatically picks the right one "
                "for your system."
            ),
        },
        {
            "option": "overrides",
            "type": "mapping",
            "label": _("DLL overrides"),
            "help": _("Sets WINEDLLOVERRIDES when launching the game."),
        },
        {
            "option": "show_debug",
            "label": _("Output debugging info"),
            "type": "choice",
            "choices": [
                (_("Disabled"), "-all"),
                (_("Enabled"), ""),
                (_("Inherit from environment"), "inherit"),
                (_("Show FPS"), "+fps"),
                (_("Full (CAUTION: Will cause MASSIVE slowdown)"), "+all"),
            ],
            "default": "-all",
            "help": _("Output debugging information in the game log "
                      "(might affect performance)"),
        },
        {
            "option": "ShowCrashDialog",
            "label": _("Show crash dialogs"),
            "type": "bool",
            "default": False,
            "advanced": True,
        },
        {
            "option": "autoconf_joypad",
            "type": "bool",
            "label": _("Autoconfigure joypads"),
            "advanced": True,
            "default": False,
            "help":
            _("Automatically disables one of Wine's detected joypad "
              "to avoid having 2 controllers detected"),
        },
        {
            "option": "sandbox",
            "type": "bool",
            "label": _("Create a sandbox for Wine folders"),
            "default": True,
            "advanced": True,
            "help": _(
                "Do not use $HOME for desktop integration folders.\n"
                "By default, it use the directories in the confined "
                "Windows environment."
            ),
        },
        {
            "option": "sandbox_dir",
            "type": "directory_chooser",
            "label": _("Sandbox directory"),
            "help": _("Custom directory for desktop integration folders."),
            "advanced": True,
        },
    ]
force_stop_game(self, game)

Kill WINE with kindness, or at least with -k. This seems to leave a process alive for some reason, but the caller will detect this and SIGKILL it.

Source code in lutris/runners/wine.py
def force_stop_game(self, game):
    """Kill WINE with kindness, or at least with -k. This seems to leave a process
    alive for some reason, but the caller will detect this and SIGKILL it."""
    self.run_winekill()
get_dll_overrides(self)

Return the DLLs overriden at runtime

Source code in lutris/runners/wine.py
def get_dll_overrides(self):
    """Return the DLLs overriden at runtime"""
    try:
        overrides = self.runner_config["overrides"]
    except KeyError:
        overrides = {}
    if not isinstance(overrides, dict):
        logger.warning("DLL overrides is not a mapping: %s", overrides)
        overrides = {}
    return overrides
get_dpi(self)

Return the DPI to be used by Wine; returns 96 to disable scaling, as this is Window's unscaled default DPI.

Source code in lutris/runners/wine.py
def get_dpi(self):
    """Return the DPI to be used by Wine; returns 96 to disable scaling,
    as this is Window's unscaled default DPI."""
    if bool(self.runner_config.get("Dpi")):
        explicit_dpi = self.runner_config.get("ExplicitDpi")
        if explicit_dpi == "auto":
            explicit_dpi = None
        try:
            explicit_dpi = int(explicit_dpi)
        except ValueError:
            explicit_dpi = None
        return explicit_dpi or get_default_dpi()

    return 96
get_env(self, os_env=False)

Return environment variables used by the game

Source code in lutris/runners/wine.py
def get_env(self, os_env=False):
    """Return environment variables used by the game"""
    # Always false to runner.get_env, the default value
    # of os_env is inverted in the wine class,
    # the OS env is read later.
    env = super().get_env(False)
    if os_env:
        env.update(os.environ.copy())
    show_debug = self.runner_config.get("show_debug", "-all")
    if show_debug != "inherit":
        env["WINEDEBUG"] = show_debug
    if show_debug == "-all":
        env["DXVK_LOG_LEVEL"] = "none"
    env["WINEARCH"] = self.wine_arch
    env["WINE"] = self.get_executable()
    env["WINE_MONO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "mono")
    env["WINE_GECKO_CACHE_DIR"] = os.path.join(WINE_DIR, self.get_version(), "gecko")
    if is_gstreamer_build(self.get_executable()):
        path_64 = os.path.join(WINE_DIR, self.get_version(), "lib64/gstreamer-1.0/")
        path_32 = os.path.join(WINE_DIR, self.get_version(), "lib/gstreamer-1.0/")
        if os.path.exists(path_64) or os.path.exists(path_32):
            env["GST_PLUGIN_SYSTEM_PATH_1_0"] = path_64 + ":" + path_32
    if self.prefix_path:
        env["WINEPREFIX"] = self.prefix_path

    if not ("WINEESYNC" in env and env["WINEESYNC"] == "1"):
        env["WINEESYNC"] = "1" if self.runner_config.get("esync") else "0"

    if not ("WINEFSYNC" in env and env["WINEFSYNC"] == "1"):
        env["WINEFSYNC"] = "1" if self.runner_config.get("fsync") else "0"

    if self.runner_config.get("fsr"):
        env["WINE_FULLSCREEN_FSR"] = "1"

    if self.runner_config.get("dxvk_nvapi"):
        env["DXVK_NVAPIHACK"] = "0"

    if self.runner_config.get("battleye"):
        env["PROTON_BATTLEYE_RUNTIME"] = os.path.join(settings.RUNTIME_DIR, "battleye_runtime")

    overrides = self.get_dll_overrides()
    if overrides:
        self.dll_overrides.update(overrides)
    env["WINEDLLOVERRIDES"] = get_overrides_env(self.dll_overrides)
    return env
get_executable(self, version=None, fallback=True)

Return the path to the Wine executable. A specific version can be specified if needed.

Source code in lutris/runners/wine.py
def get_executable(self, version=None, fallback=True):
    """Return the path to the Wine executable.
    A specific version can be specified if needed.
    """
    if version is None:
        version = self.get_version()
    if not version:
        return

    wine_path = self.get_path_for_version(version)
    if system.path_exists(wine_path):
        return wine_path

    if fallback:
        # Fallback to default version
        default_version = get_default_version()
        wine_path = self.get_path_for_version(default_version)
        if wine_path:
            # Update the version in the config
            if version == self.runner_config.get("version"):
                self.runner_config["version"] = default_version
                # TODO: runner_config is a dict so we have to instanciate a
                # LutrisConfig object to save it.
                # XXX: The version key could be either in the game specific
                # config or the runner specific config. We need to know
                # which one to get the correct LutrisConfig object.
        return wine_path
get_path_for_version(self, version)

Return the absolute path of a wine executable for a given version

Source code in lutris/runners/wine.py
def get_path_for_version(self, version):
    """Return the absolute path of a wine executable for a given version"""
    if version in WINE_PATHS:
        return system.find_executable(WINE_PATHS[version])
    if "Proton" in version:
        for proton_path in get_proton_paths():
            if os.path.isfile(os.path.join(proton_path, version, "dist/bin/wine")):
                return os.path.join(proton_path, version, "dist/bin/wine")
    if version.startswith("PlayOnLinux"):
        version, arch = version.split()[1].rsplit("-", 1)
        return os.path.join(POL_PATH, "wine", "linux-" + arch, version, "bin/wine")
    if version == "custom":
        return self.runner_config.get("custom_wine_path", "")
    return os.path.join(WINE_DIR, version, "bin/wine")
get_pids(self, wine_path=None)

Return a list of pids of processes using the current wine exe.

Source code in lutris/runners/wine.py
def get_pids(self, wine_path=None):
    """Return a list of pids of processes using the current wine exe."""
    if wine_path:
        exe = wine_path
    else:
        exe = self.get_executable()
    if not exe.startswith("/"):
        exe = system.find_executable(exe)
    pids = system.get_pids_using_file(exe)
    if self.wine_arch == "win64" and os.path.basename(exe) == "wine":
        pids = pids | system.get_pids_using_file(exe + "64")

    # Add wineserver PIDs to the mix (at least one occurence of fuser not
    # picking the games's PID from wine/wine64 but from wineserver for some
    # unknown reason.
    pids = pids | system.get_pids_using_file(os.path.join(os.path.dirname(exe), "wineserver"))
    return pids
get_runtime_env(self)

Return runtime environment variables with path to wine for Lutris builds

Source code in lutris/runners/wine.py
def get_runtime_env(self):
    """Return runtime environment variables with path to wine for Lutris builds"""
    wine_path = self.get_executable()
    wine_root = None
    if WINE_DIR:
        wine_root = os.path.dirname(os.path.dirname(wine_path))
    for proton_path in get_proton_paths():
        if proton_path in wine_path:
            wine_root = os.path.dirname(os.path.dirname(wine_path))
    return runtime.get_env(
        version="Ubuntu-18.04",
        prefer_system_libs=self.system_config.get("prefer_system_libs", True),
        wine_path=wine_root,
    )
get_version(self, use_default=True)

Return the Wine version to use. use_default can be set to false to force the installation of a specific wine version

Source code in lutris/runners/wine.py
def get_version(self, use_default=True):
    """Return the Wine version to use. use_default can be set to false to
    force the installation of a specific wine version"""
    runner_version = self.runner_config.get("version")
    if runner_version:
        return runner_version
    if use_default:
        return get_default_version()
is_installed(self, version=None, fallback=True, min_version=None)

Check if Wine is installed. If no version is passed, checks if any version of wine is available

Source code in lutris/runners/wine.py
def is_installed(self, version=None, fallback=True, min_version=None):
    """Check if Wine is installed.
    If no version is passed, checks if any version of wine is available
    """
    if version:
        return system.path_exists(self.get_executable(version, fallback))

    wine_versions = get_wine_versions()
    if min_version:
        min_version_list, _, _ = parse_version(min_version)
        for wine_version in wine_versions:
            version_list, _, _ = parse_version(wine_version)
            if version_list > min_version_list:
                return True
        logger.warning("Wine %s or higher not found", min_version)
    return bool(wine_versions)
msi_exec(msi_file, quiet=False, prefix=None, wine_path=None, working_dir=None, blocking=False) classmethod
Source code in lutris/runners/wine.py
@classmethod
def msi_exec(
    cls,
    msi_file,
    quiet=False,
    prefix=None,
    wine_path=None,
    working_dir=None,
    blocking=False,
):
    msi_args = "/i %s" % msi_file
    if quiet:
        msi_args += " /q"
    return wineexec(
        "msiexec",
        args=msi_args,
        prefix=prefix,
        wine_path=wine_path,
        working_dir=working_dir,
        blocking=blocking,
    )
parse_wine_path(path, prefix_path=None) staticmethod

Take a Windows path, return the corresponding Linux path.

Source code in lutris/runners/wine.py
@staticmethod
def parse_wine_path(path, prefix_path=None):
    """Take a Windows path, return the corresponding Linux path."""
    if not prefix_path:
        prefix_path = os.path.expanduser("~/.wine")

    path = path.replace("\\\\", "/").replace("\\", "/")

    if path[1] == ":":  # absolute path
        drive = os.path.join(prefix_path, "dosdevices", path[:2].lower())
        if os.path.islink(drive):  # Try to resolve the path
            drive = os.readlink(drive)
        return os.path.join(drive, path[3:])

    if path[0] == "/":  # drive-relative path. C is as good a guess as any..
        return os.path.join(prefix_path, "drive_c", path[1:])

    # Relative path
    return path
play(self)
Source code in lutris/runners/wine.py
def play(self):  # pylint: disable=too-many-return-statements # noqa: C901
    game_exe = self.game_exe
    arguments = self.game_config.get("args", "")
    launch_info = {"env": self.get_env(os_env=False)}
    using_dxvk = self.runner_config.get("dxvk")

    if using_dxvk:
        # Set this to 1 to enable access to more RAM for 32bit applications
        launch_info["env"]["WINE_LARGE_ADDRESS_AWARE"] = "1"
        if not is_vulkan_supported():
            if not display_vulkan_error(True):
                return {"error": "VULKAN_NOT_FOUND"}

    if not game_exe or not system.path_exists(game_exe):
        return {"error": "FILE_NOT_FOUND", "file": game_exe}

    if launch_info["env"].get("WINEESYNC") == "1":
        limit_set = is_esync_limit_set()
        wine_ver = is_version_esync(self.get_executable())

        if not limit_set and not wine_ver:
            esync_display_version_warning(True)
            esync_display_limit_warning()
            return {"error": "ESYNC_LIMIT_NOT_SET"}
        if not is_esync_limit_set():
            esync_display_limit_warning()
            return {"error": "ESYNC_LIMIT_NOT_SET"}
        if not wine_ver:
            if not esync_display_version_warning(True):
                return {"error": "NON_ESYNC_WINE_VERSION"}

    if launch_info["env"].get("WINEFSYNC") == "1":
        fsync_supported = is_fsync_supported()
        wine_ver = is_version_fsync(self.get_executable())

        if not fsync_supported and not wine_ver:
            fsync_display_version_warning(True)
            fsync_display_support_warning()
            return {"error": "FSYNC_NOT_SUPPORTED"}
        if not fsync_supported:
            fsync_display_support_warning()
            return {"error": "FSYNC_NOT_SUPPORTED"}
        if not wine_ver:
            if not fsync_display_version_warning(True):
                return {"error": "NON_FSYNC_WINE_VERSION"}

    command = [self.get_executable()]

    game_exe, args, _working_dir = get_real_executable(game_exe, self.working_dir)
    command.append(game_exe)
    if args:
        command = command + args

    if arguments:
        for arg in split_arguments(arguments):
            command.append(arg)
    launch_info["command"] = command
    return launch_info
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/wine.py
def prelaunch(self):
    if not system.path_exists(os.path.join(self.prefix_path, "user.reg")):
        logger.warning("No valid prefix detected in %s, creating one...", self.prefix_path)
        create_prefix(self.prefix_path, wine_path=self.get_executable(), arch=self.wine_arch)

    prefix_manager = WinePrefixManager(self.prefix_path)
    if self.runner_config.get("autoconf_joypad", False):
        prefix_manager.configure_joypads()
    self.sandbox(prefix_manager)
    self.set_regedit_keys()

    self.setup_dlls(
        DXVKManager,
        bool(self.runner_config.get("dxvk")),
        self.runner_config.get("dxvk_version")
    )
    self.setup_dlls(
        VKD3DManager,
        bool(self.runner_config.get("vkd3d")),
        self.runner_config.get("vkd3d_version")
    )
    self.setup_dlls(
        DXVKNVAPIManager,
        bool(self.runner_config.get("dxvk_nvapi")),
        self.runner_config.get("dxvk_nvapi_version")
    )
    self.setup_dlls(
        D3DExtrasManager,
        bool(self.runner_config.get("d3d_extras")),
        self.runner_config.get("d3d_extras_version")
    )
    self.setup_dlls(
        dgvoodoo2Manager,
        bool(self.runner_config.get("dgvoodoo2")),
        self.runner_config.get("dgvoodoo2_version")
    )
    return True
run_regedit(self, *args)

Run regedit in the current context

Source code in lutris/runners/wine.py
def run_regedit(self, *args):
    """Run regedit in the current context"""
    self.prelaunch()
    self._run_executable("regedit")
run_wine_terminal(self, *args)
Source code in lutris/runners/wine.py
def run_wine_terminal(self, *args):
    terminal = self.system_config.get("terminal_app")
    open_wine_terminal(
        terminal=terminal,
        wine_path=self.get_executable(),
        prefix=self.prefix_path,
        env=self.get_env(os_env=True)
    )
run_winecfg(self, *args)

Run winecfg in the current context

Source code in lutris/runners/wine.py
def run_winecfg(self, *args):
    """Run winecfg in the current context"""
    self.prelaunch()
    winecfg(
        wine_path=self.get_executable(),
        prefix=self.prefix_path,
        arch=self.wine_arch,
        config=self,
        env=self.get_env(os_env=True),
    )
run_wineconsole(self, *args)

Runs wineconsole inside wine prefix.

Source code in lutris/runners/wine.py
def run_wineconsole(self, *args):
    """Runs wineconsole inside wine prefix."""
    self.prelaunch()
    self._run_executable("wineconsole")
run_winecpl(self, *args)

Execute Wine control panel.

Source code in lutris/runners/wine.py
def run_winecpl(self, *args):
    """Execute Wine control panel."""
    self.prelaunch()
    self._run_executable("control")
run_wineexec(self, *args)

Ask the user for an arbitrary exe file to run in the game's prefix

Source code in lutris/runners/wine.py
def run_wineexec(self, *args):
    """Ask the user for an arbitrary exe file to run in the game's prefix"""
    dlg = FileDialog(_("Select an EXE or MSI file"), default_path=self.game_path)
    filename = dlg.filename
    if not filename:
        return
    self.prelaunch()
    self._run_executable(filename)
run_winekill(self, *args)

Runs wineserver -k.

Source code in lutris/runners/wine.py
def run_winekill(self, *args):
    """Runs wineserver -k."""
    winekill(
        self.prefix_path,
        arch=self.wine_arch,
        wine_path=self.get_executable(),
        env=self.get_env(),
        initial_pids=self.get_pids(),
    )
    return True
run_winetricks(self, *args)

Run winetricks in the current context

Source code in lutris/runners/wine.py
def run_winetricks(self, *args):
    """Run winetricks in the current context"""
    self.prelaunch()
    winetricks(
        "", prefix=self.prefix_path, wine_path=self.get_executable(), config=self, env=self.get_env(os_env=True)
    )
sandbox(self, wine_prefix)
Source code in lutris/runners/wine.py
def sandbox(self, wine_prefix):
    if self.runner_config.get("sandbox", True):
        wine_prefix.desktop_integration(desktop_dir=self.runner_config.get("sandbox_dir"))
    else:
        wine_prefix.desktop_integration(restore=True)
set_regedit_keys(self)

Reset regedit keys according to config.

Source code in lutris/runners/wine.py
def set_regedit_keys(self):
    """Reset regedit keys according to config."""
    prefix_manager = WinePrefixManager(self.prefix_path)
    # Those options are directly changed with the prefix manager and skip
    # any calls to regedit.
    managed_keys = {
        "ShowCrashDialog": prefix_manager.set_crash_dialogs,
        "Desktop": prefix_manager.set_virtual_desktop,
        "WineDesktop": prefix_manager.set_desktop_size,
    }

    for key, path in self.reg_keys.items():
        value = self.runner_config.get(key) or "auto"
        if not value or value == "auto" and key not in managed_keys:
            prefix_manager.clear_registry_subkeys(path, key)
        elif key in self.runner_config:
            if key in managed_keys:
                # Do not pass fallback 'auto' value to managed keys
                if value == "auto":
                    value = None
                managed_keys[key](value)
                continue
            # Convert numeric strings to integers so they are saved as dword
            if value.isdigit():
                value = int(value)

            prefix_manager.set_registry_key(path, key, value)

    # We always configure the DPI, because if the user turns off DPI scaling, but it
    # had been on the only way to implement that is to save 96 DPI into the registry.
    prefix_manager.set_dpi(self.get_dpi())
setup_dlls(self, manager_class, enable, version)

Enable or disable DLLs

Source code in lutris/runners/wine.py
def setup_dlls(self, manager_class, enable, version):
    """Enable or disable DLLs"""
    dll_manager = manager_class(
        self.prefix_path,
        arch=self.wine_arch,
        version=version,
    )
    # manual version only sets the dlls to native
    if dll_manager.version.lower() != "manual":
        if enable:
            dll_manager.enable()
        else:
            dll_manager.disable()

    if enable:
        for dll in dll_manager.managed_dlls:
            # We have to make sure that the dll exists before setting it to native
            if dll_manager.dll_exists(dll):
                self.dll_overrides[dll] = "n"

yuzu

yuzu (Runner)

Source code in lutris/runners/yuzu.py
class yuzu(Runner):
    human_name = _("Yuzu")
    platforms = [_("Nintendo Switch")]
    description = _("Nintendo Switch emulator")
    runnable_alone = True
    runner_executable = "yuzu/yuzu"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("ROM file"),
            "help": _("The game data, commonly called a ROM image."),
        }
    ]
    runner_options = [
        {
            "option": "prod_keys",
            "label": _("Encryption keys"),
            "type": "file",
            "help": _("File containing the encryption keys."),
        }, {
            "option": "title_keys",
            "label": _("Title keys"),
            "type": "file",
            "help": _("File containing the title keys."),
        }
    ]

    @property
    def yuzu_data_dir(self):
        """Return dir where Yuzu files lie."""
        candidates = ("~/.local/share/yuzu", )
        for candidate in candidates:
            path = system.fix_path_case(os.path.join(os.path.expanduser(candidate), "nand"))
            if path and system.path_exists(path):
                return path[:-len("nand")]

    def play(self):
        """Run the game."""
        arguments = [self.get_executable()]
        rom = self.game_config.get("main_file") or ""
        if not system.path_exists(rom):
            return {"error": "FILE_NOT_FOUND", "file": rom}
        arguments.append(rom)
        return {"command": arguments}

    def _update_key(self, key_type):
        """Update a keys file if set """
        yuzu_data_dir = self.yuzu_data_dir
        if not yuzu_data_dir:
            logger.error("Yuzu data dir not set")
            return
        if key_type == "prod_keys":
            key_loc = os.path.join(yuzu_data_dir, "keys/prod.keys")
        elif key_type == "title_keys":
            key_loc = os.path.join(yuzu_data_dir, "keys/title.keys")
        else:
            logger.error("Invalid keys type %s!", key_type)
            return

        key = self.runner_config.get(key_type)
        if not key:
            logger.debug("No %s file was set.", key_type)
            return
        if not system.path_exists(key):
            logger.warning("Keys file %s does not exist!", key)
            return

        keys_dir = os.path.dirname(key_loc)
        if not os.path.exists(keys_dir):
            os.makedirs(keys_dir)
        elif os.path.isfile(key_loc) and filecmp.cmp(key, key_loc):
            # If the files are identical, don't do anything
            return
        copyfile(key, key_loc)

    def prelaunch(self):
        for key in ["prod_keys", "title_keys"]:
            self._update_key(key_type=key)
        return True
description
game_options
human_name
platforms
runnable_alone
runner_executable
runner_options
yuzu_data_dir property readonly

Return dir where Yuzu files lie.

play(self)

Run the game.

Source code in lutris/runners/yuzu.py
def play(self):
    """Run the game."""
    arguments = [self.get_executable()]
    rom = self.game_config.get("main_file") or ""
    if not system.path_exists(rom):
        return {"error": "FILE_NOT_FOUND", "file": rom}
    arguments.append(rom)
    return {"command": arguments}
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/yuzu.py
def prelaunch(self):
    for key in ["prod_keys", "title_keys"]:
        self._update_key(key_type=key)
    return True

zdoom

zdoom (Runner)

Source code in lutris/runners/zdoom.py
class zdoom(Runner):
    # http://zdoom.org/wiki/Command_line_parameters
    description = _("ZDoom DOOM Game Engine")
    human_name = _("ZDoom")
    platforms = [_("Linux")]
    runner_executable = "zdoom/zdoom"
    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": _("WAD file"),
            "help": _("The game data, commonly called a WAD file."),
        },
        {
            "option": "args",
            "type": "string",
            "label": _("Arguments"),
            "help": _("Command line arguments used when launching the game."),
        },
        {
            "option": "files",
            "type": "multiple",
            "label": _("PWAD files"),
            "help": _("Used to load one or more PWAD files which generally contain "
                      "user-created levels."),
        },
        {
            "option": "warp",
            "type": "string",
            "label": _("Warp to map"),
            "help": _("Starts the game on the given map."),
        },
        {
            "option": "savedir",
            "type": "directory_chooser",
            "label": _("Save path"),
            "help": _("User-specified path where save files should be located."),
        },
    ]
    runner_options = [
        {
            "option": "2",
            "label": _("Pixel Doubling"),
            "type": "bool",
            "default": False
        },
        {
            "option": "4",
            "label": _("Pixel Quadrupling"),
            "type": "bool",
            "default": False
        },
        {
            "option": "nostartup",
            "label": _("Disable Startup Screens"),
            "type": "bool",
            "default": False,
        },
        {
            "option": "skill",
            "label": _("Skill"),
            "type": "choice",
            "default": "",
            "choices": {
                (_("None"), ""),
                (_("I'm Too Young To Die (1)"), "1"),
                (_("Hey, Not Too Rough (2)"), "2"),
                (_("Hurt Me Plenty (3)"), "3"),
                (_("Ultra-Violence (4)"), "4"),
                (_("Nightmare! (5)"), "5"),
            },
        },
        {
            "option":
            "config",
            "label":
            _("Config file"),
            "type":
            "file",
            "help": _(
                "Used to load a user-created configuration file. If specified, "
                "the file must contain the wad directory list or launch will fail."
            ),
        },
    ]

    def get_executable(self):
        executable = super().get_executable()
        executable_dir = os.path.dirname(executable)
        if not system.path_exists(executable_dir):
            return executable
        if not system.path_exists(executable):
            gzdoom_executable = os.path.join(executable_dir, "gzdoom")
            if system.path_exists(gzdoom_executable):
                return gzdoom_executable
        return executable

    def prelaunch(self):
        if not LINUX_SYSTEM.get_soundfonts():
            logger.warning("FluidSynth is not installed, you might not have any music")
        return True

    @property
    def working_dir(self):
        wad = self.game_config.get("main_file")
        if wad:
            return os.path.dirname(os.path.expanduser(wad))
        wad_files = self.game_config.get("files")
        if wad_files:
            return os.path.dirname(os.path.expanduser(wad_files[0]))

    def play(self):  # noqa: C901
        command = [self.get_executable()]

        resolution = self.runner_config.get("resolution")
        if resolution:
            if resolution == "desktop":
                width, height = display.DISPLAY_MANAGER.get_current_resolution()
            else:
                width, height = resolution.split("x")
            command.append("-width")
            command.append(width)
            command.append("-height")
            command.append(height)

        # Append any boolean options.
        bool_options = ["2", "4", "nostartup"]
        for option in bool_options:
            if self.runner_config.get(option):
                command.append("-%s" % option)

        # Append the skill level.
        skill = self.runner_config.get("skill")
        if skill:
            command.append("-skill")
            command.append(skill)

        # Append directory for configuration file, if provided.
        config = self.runner_config.get("config")
        if config:
            command.append("-config")
            command.append(config)

        # Append the warp arguments.
        warp = self.game_config.get("warp")
        if warp:
            command.append("-warp")
            for warparg in warp.split(" "):
                command.append(warparg)

        # Append directory for save games, if provided.
        savedir = self.game_config.get("savedir")
        if savedir:
            command.append("-savedir")
            command.append(savedir)

        # Append the wad file to load, if provided.
        wad = self.game_config.get("main_file")
        if wad:
            command.append("-iwad")
            command.append(wad)

        # Append the pwad files to load, if provided.
        files = self.game_config.get("files") or []
        pwads = [f for f in files if f.lower().endswith(".wad") or f.lower().endswith(".pk3")]
        deh = [f for f in files if f.lower().endswith(".deh")]
        bex = [f for f in files if f.lower().endswith(".bex")]
        if deh:
            command.append("-deh")
            command.append(deh[0])
        if bex:
            command.append("-bex")
            command.append(bex[0])
        if pwads:
            command.append("-file")
            for pwad in pwads:
                command.append(pwad)

        # Append additional arguments, if provided.
        args = self.game_config.get("args") or ""
        for arg in split_arguments(args):
            command.append(arg)

        return {"command": command}
description
game_options
human_name
platforms
runner_executable
runner_options
working_dir property readonly

Return the working directory to use when running the game.

get_executable(self)
Source code in lutris/runners/zdoom.py
def get_executable(self):
    executable = super().get_executable()
    executable_dir = os.path.dirname(executable)
    if not system.path_exists(executable_dir):
        return executable
    if not system.path_exists(executable):
        gzdoom_executable = os.path.join(executable_dir, "gzdoom")
        if system.path_exists(gzdoom_executable):
            return gzdoom_executable
    return executable
play(self)
Source code in lutris/runners/zdoom.py
def play(self):  # noqa: C901
    command = [self.get_executable()]

    resolution = self.runner_config.get("resolution")
    if resolution:
        if resolution == "desktop":
            width, height = display.DISPLAY_MANAGER.get_current_resolution()
        else:
            width, height = resolution.split("x")
        command.append("-width")
        command.append(width)
        command.append("-height")
        command.append(height)

    # Append any boolean options.
    bool_options = ["2", "4", "nostartup"]
    for option in bool_options:
        if self.runner_config.get(option):
            command.append("-%s" % option)

    # Append the skill level.
    skill = self.runner_config.get("skill")
    if skill:
        command.append("-skill")
        command.append(skill)

    # Append directory for configuration file, if provided.
    config = self.runner_config.get("config")
    if config:
        command.append("-config")
        command.append(config)

    # Append the warp arguments.
    warp = self.game_config.get("warp")
    if warp:
        command.append("-warp")
        for warparg in warp.split(" "):
            command.append(warparg)

    # Append directory for save games, if provided.
    savedir = self.game_config.get("savedir")
    if savedir:
        command.append("-savedir")
        command.append(savedir)

    # Append the wad file to load, if provided.
    wad = self.game_config.get("main_file")
    if wad:
        command.append("-iwad")
        command.append(wad)

    # Append the pwad files to load, if provided.
    files = self.game_config.get("files") or []
    pwads = [f for f in files if f.lower().endswith(".wad") or f.lower().endswith(".pk3")]
    deh = [f for f in files if f.lower().endswith(".deh")]
    bex = [f for f in files if f.lower().endswith(".bex")]
    if deh:
        command.append("-deh")
        command.append(deh[0])
    if bex:
        command.append("-bex")
        command.append(bex[0])
    if pwads:
        command.append("-file")
        for pwad in pwads:
            command.append(pwad)

    # Append additional arguments, if provided.
    args = self.game_config.get("args") or ""
    for arg in split_arguments(args):
        command.append(arg)

    return {"command": command}
prelaunch(self)

Run actions before running the game, override this method in runners

Source code in lutris/runners/zdoom.py
def prelaunch(self):
    if not LINUX_SYSTEM.get_soundfonts():
        logger.warning("FluidSynth is not installed, you might not have any music")
    return True

runtime

Runtime handling module

DEFAULT_RUNTIME

RUNTIME_DISABLED

Runtime

Class for manipulating runtime folders

Source code in lutris/runtime.py
class Runtime:

    """Class for manipulating runtime folders"""

    def __init__(self, name, updater):
        self.name = name
        self.updater = updater

    @property
    def local_runtime_path(self):
        """Return the local path for the runtime folder"""
        if not self.name:
            return None
        return os.path.join(settings.RUNTIME_DIR, self.name)

    def get_updated_at(self):
        """Return the modification date of the runtime folder"""
        if not system.path_exists(self.local_runtime_path):
            return None
        return time.gmtime(os.path.getmtime(self.local_runtime_path))

    def set_updated_at(self):
        """Set the creation and modification time to now"""
        if not system.path_exists(self.local_runtime_path):
            logger.error("No local runtime path in %s", self.local_runtime_path)
            return
        os.utime(self.local_runtime_path)

    def should_update(self, remote_updated_at):
        """Determine if the current runtime should be updated"""
        local_updated_at = self.get_updated_at()
        if not local_updated_at:
            logger.warning("Runtime %s is not available locally", self.name)
            return True

        if local_updated_at and local_updated_at >= remote_updated_at:
            return False

        logger.debug(
            "Runtime %s locally updated on %s, remote created on %s)",
            self.name,
            time.strftime("%c", local_updated_at),
            time.strftime("%c", remote_updated_at),
        )
        return True

    def should_update_component(self, filename, remote_modified_at):
        """Should an individual component be updated?"""
        file_path = os.path.join(settings.RUNTIME_DIR, self.name, filename)
        if not system.path_exists(file_path):
            return True
        locally_modified_at = time.gmtime(os.path.getmtime(file_path))
        if locally_modified_at >= remote_modified_at:
            return False
        return True

    def download(self, remote_runtime_info):
        """Downloads a runtime locally"""
        url = remote_runtime_info["url"]
        if not url:
            return self.download_components()
        remote_updated_at = remote_runtime_info["created_at"]
        remote_updated_at = time.strptime(remote_updated_at[:remote_updated_at.find(".")], "%Y-%m-%dT%H:%M:%S")
        if not self.should_update(remote_updated_at):
            return None

        archive_path = os.path.join(settings.RUNTIME_DIR, os.path.basename(url))
        downloader = Downloader(url, archive_path, overwrite=True)
        downloader.start()
        GLib.timeout_add(100, self.check_download_progress, downloader)
        return downloader

    def download_component(self, component):
        """Download an individual file from a runtime item"""
        file_path = os.path.join(settings.RUNTIME_DIR, self.name, component["filename"])
        try:
            http.download_file(component["url"], file_path)
        except http.HTTPError as ex:
            logger.error("Failed to download runtime component %s: %s", component, ex)
            return
        return file_path

    def get_runtime_components(self):
        """Fetch runtime components from the API"""
        request = http.Request(settings.RUNTIME_URL + "/" + self.name)
        try:
            response = request.get()
        except http.HTTPError as ex:
            logger.error("Failed to get components: %s", ex)
            return []
        if not response.json:
            return []
        return response.json.get("components", [])

    def download_components(self):
        """Download a runtime item by individual components."""
        components = self.get_runtime_components()
        downloads = []
        for component in components:
            modified_at = time.strptime(
                component["modified_at"][:component["modified_at"].find(".")], "%Y-%m-%dT%H:%M:%S"
            )
            if not self.should_update_component(component["filename"], modified_at):
                continue
            downloads.append(component)

        with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
            future_downloads = {
                executor.submit(self.download_component, component): component["filename"]
                for component in downloads
            }
            for future in concurrent.futures.as_completed(future_downloads):
                filename = future_downloads[future]
                if not filename:
                    logger.warning("Failed to get %s", future)

    def check_download_progress(self, downloader):
        """Call download.check_progress(), return True if download finished."""
        if not downloader or downloader.state in [
            downloader.CANCELLED,
            downloader.ERROR,
        ]:
            logger.debug("Runtime update interrupted")
            return False

        downloader.check_progress()
        if downloader.state == downloader.COMPLETED:
            self.on_downloaded(downloader.dest)
            return False
        return True

    def on_downloaded(self, path):
        """Actions taken once a runtime is downloaded

        Arguments:
            path (str): local path to the runtime archive
        """
        stats = os.stat(path)
        if not stats.st_size:
            logger.error("Download failed: file %s is empty, Deleting file.", path)
            os.unlink(path)
            self.updater.notify_finish(self)
            return False
        directory, _filename = os.path.split(path)

        # Delete the existing runtime path
        initial_path = os.path.join(directory, self.name)
        system.remove_folder(initial_path)

        # Extract the runtime archive
        jobs.AsyncCall(extract_archive, self.on_extracted, path, settings.RUNTIME_DIR, merge_single=False)
        return False

    def on_extracted(self, result, error):
        """Callback method when a runtime has extracted"""
        if error:
            logger.error("Runtime update failed")
            logger.error(error)
            self.updater.notify_finish(self)
            return False
        archive_path, _destination_path = result
        logger.debug("Deleting runtime archive %s", archive_path)
        os.unlink(archive_path)
        self.set_updated_at()
        self.updater.notify_finish(self)
        return False

local_runtime_path property readonly

Return the local path for the runtime folder

__init__(self, name, updater) special

Source code in lutris/runtime.py
def __init__(self, name, updater):
    self.name = name
    self.updater = updater

check_download_progress(self, downloader)

Call download.check_progress(), return True if download finished.

Source code in lutris/runtime.py
def check_download_progress(self, downloader):
    """Call download.check_progress(), return True if download finished."""
    if not downloader or downloader.state in [
        downloader.CANCELLED,
        downloader.ERROR,
    ]:
        logger.debug("Runtime update interrupted")
        return False

    downloader.check_progress()
    if downloader.state == downloader.COMPLETED:
        self.on_downloaded(downloader.dest)
        return False
    return True

download(self, remote_runtime_info)

Downloads a runtime locally

Source code in lutris/runtime.py
def download(self, remote_runtime_info):
    """Downloads a runtime locally"""
    url = remote_runtime_info["url"]
    if not url:
        return self.download_components()
    remote_updated_at = remote_runtime_info["created_at"]
    remote_updated_at = time.strptime(remote_updated_at[:remote_updated_at.find(".")], "%Y-%m-%dT%H:%M:%S")
    if not self.should_update(remote_updated_at):
        return None

    archive_path = os.path.join(settings.RUNTIME_DIR, os.path.basename(url))
    downloader = Downloader(url, archive_path, overwrite=True)
    downloader.start()
    GLib.timeout_add(100, self.check_download_progress, downloader)
    return downloader

download_component(self, component)

Download an individual file from a runtime item

Source code in lutris/runtime.py
def download_component(self, component):
    """Download an individual file from a runtime item"""
    file_path = os.path.join(settings.RUNTIME_DIR, self.name, component["filename"])
    try:
        http.download_file(component["url"], file_path)
    except http.HTTPError as ex:
        logger.error("Failed to download runtime component %s: %s", component, ex)
        return
    return file_path

download_components(self)

Download a runtime item by individual components.

Source code in lutris/runtime.py
def download_components(self):
    """Download a runtime item by individual components."""
    components = self.get_runtime_components()
    downloads = []
    for component in components:
        modified_at = time.strptime(
            component["modified_at"][:component["modified_at"].find(".")], "%Y-%m-%dT%H:%M:%S"
        )
        if not self.should_update_component(component["filename"], modified_at):
            continue
        downloads.append(component)

    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
        future_downloads = {
            executor.submit(self.download_component, component): component["filename"]
            for component in downloads
        }
        for future in concurrent.futures.as_completed(future_downloads):
            filename = future_downloads[future]
            if not filename:
                logger.warning("Failed to get %s", future)

get_runtime_components(self)

Fetch runtime components from the API

Source code in lutris/runtime.py
def get_runtime_components(self):
    """Fetch runtime components from the API"""
    request = http.Request(settings.RUNTIME_URL + "/" + self.name)
    try:
        response = request.get()
    except http.HTTPError as ex:
        logger.error("Failed to get components: %s", ex)
        return []
    if not response.json:
        return []
    return response.json.get("components", [])

get_updated_at(self)

Return the modification date of the runtime folder

Source code in lutris/runtime.py
def get_updated_at(self):
    """Return the modification date of the runtime folder"""
    if not system.path_exists(self.local_runtime_path):
        return None
    return time.gmtime(os.path.getmtime(self.local_runtime_path))

on_downloaded(self, path)

Actions taken once a runtime is downloaded

Parameters:

Name Type Description Default
path str

local path to the runtime archive

required
Source code in lutris/runtime.py
def on_downloaded(self, path):
    """Actions taken once a runtime is downloaded

    Arguments:
        path (str): local path to the runtime archive
    """
    stats = os.stat(path)
    if not stats.st_size:
        logger.error("Download failed: file %s is empty, Deleting file.", path)
        os.unlink(path)
        self.updater.notify_finish(self)
        return False
    directory, _filename = os.path.split(path)

    # Delete the existing runtime path
    initial_path = os.path.join(directory, self.name)
    system.remove_folder(initial_path)

    # Extract the runtime archive
    jobs.AsyncCall(extract_archive, self.on_extracted, path, settings.RUNTIME_DIR, merge_single=False)
    return False

on_extracted(self, result, error)

Callback method when a runtime has extracted

Source code in lutris/runtime.py
def on_extracted(self, result, error):
    """Callback method when a runtime has extracted"""
    if error:
        logger.error("Runtime update failed")
        logger.error(error)
        self.updater.notify_finish(self)
        return False
    archive_path, _destination_path = result
    logger.debug("Deleting runtime archive %s", archive_path)
    os.unlink(archive_path)
    self.set_updated_at()
    self.updater.notify_finish(self)
    return False

set_updated_at(self)

Set the creation and modification time to now

Source code in lutris/runtime.py
def set_updated_at(self):
    """Set the creation and modification time to now"""
    if not system.path_exists(self.local_runtime_path):
        logger.error("No local runtime path in %s", self.local_runtime_path)
        return
    os.utime(self.local_runtime_path)

should_update(self, remote_updated_at)

Determine if the current runtime should be updated

Source code in lutris/runtime.py
def should_update(self, remote_updated_at):
    """Determine if the current runtime should be updated"""
    local_updated_at = self.get_updated_at()
    if not local_updated_at:
        logger.warning("Runtime %s is not available locally", self.name)
        return True

    if local_updated_at and local_updated_at >= remote_updated_at:
        return False

    logger.debug(
        "Runtime %s locally updated on %s, remote created on %s)",
        self.name,
        time.strftime("%c", local_updated_at),
        time.strftime("%c", remote_updated_at),
    )
    return True

should_update_component(self, filename, remote_modified_at)

Should an individual component be updated?

Source code in lutris/runtime.py
def should_update_component(self, filename, remote_modified_at):
    """Should an individual component be updated?"""
    file_path = os.path.join(settings.RUNTIME_DIR, self.name, filename)
    if not system.path_exists(file_path):
        return True
    locally_modified_at = time.gmtime(os.path.getmtime(file_path))
    if locally_modified_at >= remote_modified_at:
        return False
    return True

RuntimeUpdater

Class handling the runtime updates

Source code in lutris/runtime.py
class RuntimeUpdater:
    """Class handling the runtime updates"""

    current_updates = 0
    status_updater = None

    def is_updating(self):
        """Return True if the update process is running"""
        return self.current_updates > 0

    def update(self):
        """Launch the update process"""
        if RUNTIME_DISABLED:
            logger.warning("Runtime disabled, not updating it.")
            return 0

        for remote_runtime in self._iter_remote_runtimes():
            runtime = Runtime(remote_runtime["name"], self)
            downloader = runtime.download(remote_runtime)
            if downloader:
                self.current_updates += 1
        return self.current_updates

    @staticmethod
    def _iter_remote_runtimes():
        request = http.Request(settings.RUNTIME_URL + "?enabled=1")
        try:
            response = request.get()
        except http.HTTPError as ex:
            logger.error("Failed to get runtimes: %s", ex)
            return
        runtimes = response.json or []
        for runtime in runtimes:

            # Skip 32bit runtimes on 64 bit systems except the main runtime
            if (
                runtime["architecture"] == "i386" and LINUX_SYSTEM.is_64_bit
                and not runtime["name"].startswith(("Ubuntu", "lib32"))
            ):
                logger.debug(
                    "Skipping runtime %s for %s",
                    runtime["name"],
                    runtime["architecture"],
                )
                continue

            # Skip 64bit runtimes on 32 bit systems
            if runtime["architecture"] == "x86_64" and not LINUX_SYSTEM.is_64_bit:
                logger.debug(
                    "Skipping runtime %s for %s",
                    runtime["name"],
                    runtime["architecture"],
                )
                continue

            yield runtime

    def notify_finish(self, runtime):
        """A runtime has finished downloading"""
        logger.debug("Runtime %s is now updated and available", runtime.name)
        self.current_updates -= 1
        if self.current_updates == 0:
            logger.info("Runtime is fully updated.")

current_updates

status_updater

is_updating(self)

Return True if the update process is running

Source code in lutris/runtime.py
def is_updating(self):
    """Return True if the update process is running"""
    return self.current_updates > 0

notify_finish(self, runtime)

A runtime has finished downloading

Source code in lutris/runtime.py
def notify_finish(self, runtime):
    """A runtime has finished downloading"""
    logger.debug("Runtime %s is now updated and available", runtime.name)
    self.current_updates -= 1
    if self.current_updates == 0:
        logger.info("Runtime is fully updated.")

update(self)

Launch the update process

Source code in lutris/runtime.py
def update(self):
    """Launch the update process"""
    if RUNTIME_DISABLED:
        logger.warning("Runtime disabled, not updating it.")
        return 0

    for remote_runtime in self._iter_remote_runtimes():
        runtime = Runtime(remote_runtime["name"], self)
        downloader = runtime.download(remote_runtime)
        if downloader:
            self.current_updates += 1
    return self.current_updates

get_env(version=None, prefer_system_libs=False, wine_path=None)

Return a dict containing LD_LIBRARY_PATH env var

Parameters:

Name Type Description Default
version str

Version of the runtime to use, such as "Ubuntu-18.04" or "legacy"

None
prefer_system_libs bool

Whether to prioritize system libs over runtime libs

False
wine_path str

If you prioritize system libs, provide the path for a lutris wine build if one is being used. This allows Lutris to prioritize the wine libs over everything else.

None

Returns:

Type Description

dict

Source code in lutris/runtime.py
def get_env(version=None, prefer_system_libs=False, wine_path=None):
    """Return a dict containing LD_LIBRARY_PATH env var

    Params:
        version (str): Version of the runtime to use, such as "Ubuntu-18.04" or "legacy"
        prefer_system_libs (bool): Whether to prioritize system libs over runtime libs
        wine_path (str): If you prioritize system libs, provide the path for a lutris wine build
                         if one is being used. This allows Lutris to prioritize the wine libs
                         over everything else.
    Returns:
        dict
    """
    library_path = ":".join(get_paths(version=version, prefer_system_libs=prefer_system_libs, wine_path=wine_path))
    env = {}
    if library_path:
        env["LD_LIBRARY_PATH"] = library_path
        network_tools_path = os.path.join(settings.RUNTIME_DIR, "network-tools")
        env["PATH"] = "%s:%s" % (network_tools_path, os.environ["PATH"])
    return env

get_paths(version=None, prefer_system_libs=True, wine_path=None)

Return a list of paths containing the runtime libraries.

Source code in lutris/runtime.py
def get_paths(version=None, prefer_system_libs=True, wine_path=None):
    """Return a list of paths containing the runtime libraries."""
    if not RUNTIME_DISABLED:
        paths = get_runtime_paths(version=version, prefer_system_libs=prefer_system_libs, wine_path=wine_path)
    else:
        paths = []
    # Put existing LD_LIBRARY_PATH at the end
    if os.environ.get("LD_LIBRARY_PATH"):
        paths.append(os.environ["LD_LIBRARY_PATH"])
    return paths

get_runtime_paths(version=None, prefer_system_libs=True, wine_path=None)

Return Lutris runtime paths

Source code in lutris/runtime.py
def get_runtime_paths(version=None, prefer_system_libs=True, wine_path=None):
    """Return Lutris runtime paths"""
    version = version or DEFAULT_RUNTIME
    lutris_runtime_path = "%s-i686" % version
    runtime_paths = [
        lutris_runtime_path,
        "steam/i386/lib/i386-linux-gnu",
        "steam/i386/lib",
        "steam/i386/usr/lib/i386-linux-gnu",
        "steam/i386/usr/lib",
    ]

    if LINUX_SYSTEM.is_64_bit:
        lutris_runtime_path = "%s-x86_64" % version
        runtime_paths += [
            lutris_runtime_path,
            "steam/amd64/lib/x86_64-linux-gnu",
            "steam/amd64/lib",
            "steam/amd64/usr/lib/x86_64-linux-gnu",
            "steam/amd64/usr/lib",
        ]

    paths = []
    if prefer_system_libs:
        if wine_path:
            paths += get_winelib_paths(wine_path)
        paths += list(LINUX_SYSTEM.iter_lib_folders())
    # Then resolve absolute paths for the runtime
    paths += [os.path.join(settings.RUNTIME_DIR, path) for path in runtime_paths]
    return paths

get_winelib_paths(wine_path)

Return wine libraries path for a Lutris wine build

Source code in lutris/runtime.py
def get_winelib_paths(wine_path):
    """Return wine libraries path for a Lutris wine build"""
    paths = []
    # Prioritize libwine.so.1 for lutris builds
    for winelib_path in ("lib", "lib64"):
        winelib_fullpath = os.path.join(wine_path or "", winelib_path)
        if system.path_exists(winelib_fullpath):
            paths.append(winelib_fullpath)
    return paths

scanners special

lutris

detect_game_from_installer(game_folder, installer)

Source code in lutris/scanners/lutris.py
def detect_game_from_installer(game_folder, installer):
    try:
        exe_path = installer["script"]["game"].get("exe")
    except KeyError:
        exe_path = installer["script"].get("exe")
    if not exe_path:
        return
    exe_path = exe_path.replace("$GAMEDIR/", "")
    full_path = os.path.join(game_folder, exe_path)
    if os.path.exists(full_path):
        return full_path

find_game(game_folder, api_game)

Source code in lutris/scanners/lutris.py
def find_game(game_folder, api_game):
    installers = get_game_installers(api_game["slug"])
    for installer in installers:
        full_path = detect_game_from_installer(game_folder, installer)
        if full_path:
            return full_path, installer
    return None, None

find_game_folder(dirname, api_game, slugs_map)

Source code in lutris/scanners/lutris.py
def find_game_folder(dirname, api_game, slugs_map):
    if api_game["slug"] in slugs_map:
        game_folder = os.path.join(dirname, slugs_map[api_game["slug"]])
        if os.path.exists(game_folder):
            return game_folder
    for alias in api_game["aliases"]:
        if alias["slug"] in slugs_map:
            game_folder = os.path.join(dirname, slugs_map[alias["slug"]])
            if os.path.exists(game_folder):
                return game_folder

get_game_slugs_and_folders(dirname)

Scan a directory for games previously installed with lutris

Source code in lutris/scanners/lutris.py
def get_game_slugs_and_folders(dirname):
    """Scan a directory for games previously installed with lutris"""
    folders = os.listdir(dirname)
    game_folders = {}
    for folder in folders:
        if not os.path.isdir(os.path.join(dirname, folder)):
            continue
        game_folders[slugify(folder)] = folder
    return game_folders

get_used_directories()

Source code in lutris/scanners/lutris.py
def get_used_directories():
    directories = set()
    for game in get_games():
        if game['directory']:
            directories.add(game['directory'])
    return directories

install_game(installer, game_folder)

Source code in lutris/scanners/lutris.py
def install_game(installer, game_folder):
    interpreter = ScriptInterpreter(installer)
    interpreter.target_path = game_folder
    interpreter.installer.save()

scan_directory(dirname)

Source code in lutris/scanners/lutris.py
def scan_directory(dirname):
    slugs_map = get_game_slugs_and_folders(dirname)
    directories = get_used_directories()
    api_games = get_api_games(list(slugs_map.keys()))
    slugs_seen = set()
    slugs_installed = set()
    for api_game in api_games:
        if api_game["slug"] in slugs_seen:
            continue
        slugs_seen.add(api_game["slug"])
        game_folder = find_game_folder(dirname, api_game, slugs_map)
        if game_folder in directories:
            slugs_installed.add(api_game["slug"])
            continue
        full_path, installer = find_game(game_folder, api_game)
        if full_path:
            logger.info("Found %s in %s", api_game["name"], full_path)
            try:
                install_game(installer, game_folder)
            except MissingGameDependency as ex:
                logger.error("Skipped %s: %s", api_game["name"], ex)
            download_lutris_media(installer["game_slug"])
            slugs_installed.add(api_game["slug"])

    installed_map = {slug: folder for slug, folder in slugs_map.items() if slug in slugs_installed}
    missing_map = {slug: folder for slug, folder in slugs_map.items() if slug not in slugs_installed}
    return installed_map, missing_map

retroarch

EXTRA_FLAGS

ROM_FLAGS

SCANNERS

clean_rom_name(name)

Remove known flags from ROM filename and apply formatting

Source code in lutris/scanners/retroarch.py
def clean_rom_name(name):
    """Remove known flags from ROM filename and apply formatting"""
    for flag in ROM_FLAGS:
        name = name.replace(" (%s)" % flag, "")
    for flag in EXTRA_FLAGS:
        name = name.replace("[%s]" % flag, "")
    if ", The" in name:
        name = "The %s" % name.replace(", The", "")
    name = name.strip()
    return name

scan_directory(dirname)

Add a directory of ROMs as Lutris games

Source code in lutris/scanners/retroarch.py
def scan_directory(dirname):
    """Add a directory of ROMs as Lutris games"""
    files = os.listdir(dirname)
    folder_extensions = {os.path.splitext(filename)[1] for filename in files}
    core_matches = {}
    for core, core_data in RECOMMENDED_CORES.items():
        for ext in core_data.get("extensions", []):
            if ext in folder_extensions:
                core_matches[ext] = core
    added_games = []
    for filename in files:
        name, ext = os.path.splitext(filename)
        if ext not in core_matches:
            continue
        logger.info("Importing '%s'", name)
        slug = slugify(name)
        core = core_matches[ext]
        config = {
            "game": {
                "core": core_matches[ext],
                "main_file": os.path.join(dirname, filename)
            }
        }
        installer_slug = "%s-libretro-%s" % (slug, core)
        existing_game = get_games(filters={"installer_slug": installer_slug})
        if existing_game:
            game = Game(existing_game[0]["id"])
            game.remove()
        configpath = write_game_config(slug, config)
        game_id = add_game(
            name=name,
            runner="libretro",
            slug=slug,
            directory=dirname,
            installed=1,
            installer_slug=installer_slug,
            configpath=configpath
        )
        added_games.append(game_id)
    return added_games

services special

Service package

DEFAULT_SERVICES

SERVICES

WIP_SERVICES

get_enabled_services()

Source code in lutris/services/__init__.py
def get_enabled_services():
    return {
        key: _class for key, _class in SERVICES.items()
        if settings.read_setting(key, section="services").lower() == "true"
    }

get_services()

Return a mapping of available services

Source code in lutris/services/__init__.py
def get_services():
    """Return a mapping of available services"""
    _services = {
        "lutris": LutrisService,
        "xdg": XDGService,
        "gog": GOGService,
        "humblebundle": HumbleBundleService,
        "egs": EpicGamesStoreService,
        "origin": OriginService,
        "ubisoft": UbisoftConnectService,
    }
    if LINUX_SYSTEM.has_steam:
        _services["steam"] = SteamService
    _services["steamwindows"] = SteamWindowsService
    if system.path_exists(DOLPHIN_GAME_CACHE_FILE):
        _services["dolphin"] = DolphinService
    return _services

base

Generic service utilities

PGA_DB

AuthTokenExpired (Exception)

Exception raised when a token is no longer valid

Source code in lutris/services/base.py
class AuthTokenExpired(Exception):
    """Exception raised when a token is no longer valid"""

BaseService (Object)

Base class for local services

Source code in lutris/services/base.py
class BaseService(GObject.Object):
    """Base class for local services"""
    id = NotImplemented
    _matcher = None
    has_extras = False
    name = NotImplemented
    icon = NotImplemented
    online = False
    local = False
    drm_free = False  # DRM free games can be added to Lutris from an existing install
    client_installer = None  # ID of a script needed to install the client used by the service
    scripts = {}  # Mapping of Javascript snippets to handle redirections during auth
    medias = {}
    extra_medias = {}
    default_format = "icon"

    __gsignals__ = {
        "service-games-load": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "service-games-loaded": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "service-login": (GObject.SIGNAL_RUN_FIRST, None, ()),
        "service-logout": (GObject.SIGNAL_RUN_FIRST, None, ()),
    }

    @property
    def matcher(self):
        if self._matcher:
            return self._matcher
        return self.id

    def run(self):
        """Override this method to run a launcher"""
        logger.warning("This service doesn't run anything")

    def is_launchable(self):
        return False

    def reload(self):
        """Refresh the service's games"""
        self.emit("service-games-load")
        try:
            self.wipe_game_cache()
            self.load()
            self.load_icons()
            self.add_installed_games()
        finally:
            self.emit("service-games-loaded")

    def load(self):
        logger.warning("Load method not implemented")

    def load_icons(self):
        """Download all game media from the service"""
        all_medias = self.medias.copy()
        all_medias.update(self.extra_medias)
        # Download icons
        for icon_type in all_medias:
            service_media = all_medias[icon_type]()
            media_urls = service_media.get_media_urls()
            download_media(media_urls, service_media)

        # Process icons
        for icon_type in all_medias:
            service_media = all_medias[icon_type]()
            service_media.render()

    def wipe_game_cache(self):
        logger.debug("Deleting games from service-games for %s", self.id)
        sql.db_delete(PGA_DB, "service_games", "service", self.id)

    def get_update_installers(self, db_game):
        return []

    def generate_installer(self, db_game):
        """Used to generate an installer from the data returned from the services"""
        return {}

    def match_game(self, service_game, api_game):
        """Match a service game to a lutris game referenced by its slug"""
        if not service_game:
            return
        sql.db_update(
            PGA_DB,
            "service_games",
            {"lutris_slug": api_game["slug"]},
            conditions={"appid": service_game["appid"], "service": self.id}
        )
        unmatched_lutris_games = get_games(
            searches={"installer_slug": self.matcher},
            filters={"slug": api_game["slug"]},
            excludes={"service": self.id}
        )
        for game in unmatched_lutris_games:
            logger.debug("Updating unmatched game %s", game)
            sql.db_update(
                PGA_DB,
                "games",
                {"service": self.id, "service_id": service_game["appid"]},
                conditions={"id": game["id"]}
            )

    def match_games(self):
        """Matching of service games to lutris games"""
        service_games = {
            str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
        }
        lutris_games = api.get_api_games(list(service_games.keys()), service=self.id)
        for lutris_game in lutris_games:
            for provider_game in lutris_game["provider_games"]:
                if provider_game["service"] != self.id:
                    continue
                self.match_game(service_games.get(provider_game["slug"]), lutris_game)
        unmatched_service_games = get_games(searches={"installer_slug": self.matcher}, excludes={"service": self.id})
        for lutris_game in api.get_api_games(game_slugs=[g["slug"] for g in unmatched_service_games]):
            for provider_game in lutris_game["provider_games"]:
                if provider_game["service"] != self.id:
                    continue
                self.match_game(service_games.get(provider_game["slug"]), lutris_game)

    def match_existing_game(self, db_games, appid):
        """Checks if a game is already installed and populates the service info"""
        for _game in db_games:
            logger.debug("Matching %s with existing install: %s", appid, _game)
            game = Game(_game["id"])
            game.appid = appid
            game.service = self.id
            game.save()
            service_game = ServiceGameCollection.get_game(self.id, appid)
            sql.db_update(PGA_DB, "service_games", {"lutris_slug": game.slug}, {"id": service_game["id"]})
            return game

    def get_installers_from_api(self, appid):
        """Query the lutris API for an appid and get existing installers for the service"""
        lutris_games = api.get_api_games([appid], service=self.id)
        service_installers = []
        if lutris_games:
            lutris_game = lutris_games[0]
            installers = get_game_installers(lutris_game["slug"])
            for installer in installers:
                if self.matcher in installer["version"].lower():
                    service_installers.append(installer)
        return service_installers

    def install(self, db_game, update=False):
        """Install a service game, or starts the installer of the game.

        Args:
            db_game (dict or str): Database fields of the game to add, or (for Lutris service only
                the slug of the game.)

        Returns:
            str: The slug of the game that was installed, to be run. None if the game should not be
                run now. Many installers start from here, but continue running after this returns;
                they return None.
        """
        appid = db_game["appid"]
        logger.debug("Installing %s from service %s", appid, self.id)

        # Local services (aka game libraries that don't require any type of online interaction) can
        # be added without going through an install dialog.
        if self.local:
            return self.simple_install(db_game)
        if update:
            service_installers = self.get_update_installers(db_game)
        else:
            service_installers = self.get_installers_from_api(appid)
        # Check if the game is not already installed
        for service_installer in service_installers:
            existing_game = self.match_existing_game(
                get_games(filters={"installer_slug": service_installer["slug"], "installed": "1"}),
                appid
            )
            if existing_game:
                logger.debug("Found existing game, aborting install")
                return
        if update:
            installer = None
        else:
            installer = self.generate_installer(db_game)
        if installer:
            if service_installers:
                installer["version"] = installer["version"] + " (auto-generated)"
            service_installers.append(installer)
        if not service_installers:
            logger.error("No installer found for %s", db_game)
            return

        application = Gio.Application.get_default()
        application.show_installer_window(service_installers, service=self, appid=appid)

    def simple_install(self, db_game):
        """A simplified version of the install method, used when a game doesn't need any setup"""
        installer = self.generate_installer(db_game)
        configpath = write_game_config(db_game["slug"], installer["script"])
        game_id = add_game(
            name=installer["name"],
            runner=installer["runner"],
            slug=installer["game_slug"],
            directory=self.get_game_directory(installer),
            installed=1,
            installer_slug=installer["slug"],
            configpath=configpath,
            service=self.id,
            service_id=db_game["appid"],
        )
        return game_id

    def add_installed_games(self):
        """Services can implement this method to scan for locally
        installed games and add them to lutris.
        """

    def get_game_directory(self, _installer):
        """Specific services should implement this"""
        return ""
client_installer
default_format
drm_free
extra_medias
has_extras
icon
id
local
matcher property readonly
medias
name
online
scripts
add_installed_games(self)

Services can implement this method to scan for locally installed games and add them to lutris.

Source code in lutris/services/base.py
def add_installed_games(self):
    """Services can implement this method to scan for locally
    installed games and add them to lutris.
    """
generate_installer(self, db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/base.py
def generate_installer(self, db_game):
    """Used to generate an installer from the data returned from the services"""
    return {}
get_game_directory(self, _installer)

Specific services should implement this

Source code in lutris/services/base.py
def get_game_directory(self, _installer):
    """Specific services should implement this"""
    return ""
get_installers_from_api(self, appid)

Query the lutris API for an appid and get existing installers for the service

Source code in lutris/services/base.py
def get_installers_from_api(self, appid):
    """Query the lutris API for an appid and get existing installers for the service"""
    lutris_games = api.get_api_games([appid], service=self.id)
    service_installers = []
    if lutris_games:
        lutris_game = lutris_games[0]
        installers = get_game_installers(lutris_game["slug"])
        for installer in installers:
            if self.matcher in installer["version"].lower():
                service_installers.append(installer)
    return service_installers
get_update_installers(self, db_game)
Source code in lutris/services/base.py
def get_update_installers(self, db_game):
    return []
install(self, db_game, update=False)

Install a service game, or starts the installer of the game.

Parameters:

Name Type Description Default
db_game dict or str

Database fields of the game to add, or (for Lutris service only the slug of the game.)

required

Returns:

Type Description
str

The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None.

Source code in lutris/services/base.py
def install(self, db_game, update=False):
    """Install a service game, or starts the installer of the game.

    Args:
        db_game (dict or str): Database fields of the game to add, or (for Lutris service only
            the slug of the game.)

    Returns:
        str: The slug of the game that was installed, to be run. None if the game should not be
            run now. Many installers start from here, but continue running after this returns;
            they return None.
    """
    appid = db_game["appid"]
    logger.debug("Installing %s from service %s", appid, self.id)

    # Local services (aka game libraries that don't require any type of online interaction) can
    # be added without going through an install dialog.
    if self.local:
        return self.simple_install(db_game)
    if update:
        service_installers = self.get_update_installers(db_game)
    else:
        service_installers = self.get_installers_from_api(appid)
    # Check if the game is not already installed
    for service_installer in service_installers:
        existing_game = self.match_existing_game(
            get_games(filters={"installer_slug": service_installer["slug"], "installed": "1"}),
            appid
        )
        if existing_game:
            logger.debug("Found existing game, aborting install")
            return
    if update:
        installer = None
    else:
        installer = self.generate_installer(db_game)
    if installer:
        if service_installers:
            installer["version"] = installer["version"] + " (auto-generated)"
        service_installers.append(installer)
    if not service_installers:
        logger.error("No installer found for %s", db_game)
        return

    application = Gio.Application.get_default()
    application.show_installer_window(service_installers, service=self, appid=appid)
is_launchable(self)
Source code in lutris/services/base.py
def is_launchable(self):
    return False
load(self)
Source code in lutris/services/base.py
def load(self):
    logger.warning("Load method not implemented")
load_icons(self)

Download all game media from the service

Source code in lutris/services/base.py
def load_icons(self):
    """Download all game media from the service"""
    all_medias = self.medias.copy()
    all_medias.update(self.extra_medias)
    # Download icons
    for icon_type in all_medias:
        service_media = all_medias[icon_type]()
        media_urls = service_media.get_media_urls()
        download_media(media_urls, service_media)

    # Process icons
    for icon_type in all_medias:
        service_media = all_medias[icon_type]()
        service_media.render()
match_existing_game(self, db_games, appid)

Checks if a game is already installed and populates the service info

Source code in lutris/services/base.py
def match_existing_game(self, db_games, appid):
    """Checks if a game is already installed and populates the service info"""
    for _game in db_games:
        logger.debug("Matching %s with existing install: %s", appid, _game)
        game = Game(_game["id"])
        game.appid = appid
        game.service = self.id
        game.save()
        service_game = ServiceGameCollection.get_game(self.id, appid)
        sql.db_update(PGA_DB, "service_games", {"lutris_slug": game.slug}, {"id": service_game["id"]})
        return game
match_game(self, service_game, api_game)

Match a service game to a lutris game referenced by its slug

Source code in lutris/services/base.py
def match_game(self, service_game, api_game):
    """Match a service game to a lutris game referenced by its slug"""
    if not service_game:
        return
    sql.db_update(
        PGA_DB,
        "service_games",
        {"lutris_slug": api_game["slug"]},
        conditions={"appid": service_game["appid"], "service": self.id}
    )
    unmatched_lutris_games = get_games(
        searches={"installer_slug": self.matcher},
        filters={"slug": api_game["slug"]},
        excludes={"service": self.id}
    )
    for game in unmatched_lutris_games:
        logger.debug("Updating unmatched game %s", game)
        sql.db_update(
            PGA_DB,
            "games",
            {"service": self.id, "service_id": service_game["appid"]},
            conditions={"id": game["id"]}
        )
match_games(self)

Matching of service games to lutris games

Source code in lutris/services/base.py
def match_games(self):
    """Matching of service games to lutris games"""
    service_games = {
        str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
    }
    lutris_games = api.get_api_games(list(service_games.keys()), service=self.id)
    for lutris_game in lutris_games:
        for provider_game in lutris_game["provider_games"]:
            if provider_game["service"] != self.id:
                continue
            self.match_game(service_games.get(provider_game["slug"]), lutris_game)
    unmatched_service_games = get_games(searches={"installer_slug": self.matcher}, excludes={"service": self.id})
    for lutris_game in api.get_api_games(game_slugs=[g["slug"] for g in unmatched_service_games]):
        for provider_game in lutris_game["provider_games"]:
            if provider_game["service"] != self.id:
                continue
            self.match_game(service_games.get(provider_game["slug"]), lutris_game)
reload(self)

Refresh the service's games

Source code in lutris/services/base.py
def reload(self):
    """Refresh the service's games"""
    self.emit("service-games-load")
    try:
        self.wipe_game_cache()
        self.load()
        self.load_icons()
        self.add_installed_games()
    finally:
        self.emit("service-games-loaded")
run(self)

Override this method to run a launcher

Source code in lutris/services/base.py
def run(self):
    """Override this method to run a launcher"""
    logger.warning("This service doesn't run anything")
simple_install(self, db_game)

A simplified version of the install method, used when a game doesn't need any setup

Source code in lutris/services/base.py
def simple_install(self, db_game):
    """A simplified version of the install method, used when a game doesn't need any setup"""
    installer = self.generate_installer(db_game)
    configpath = write_game_config(db_game["slug"], installer["script"])
    game_id = add_game(
        name=installer["name"],
        runner=installer["runner"],
        slug=installer["game_slug"],
        directory=self.get_game_directory(installer),
        installed=1,
        installer_slug=installer["slug"],
        configpath=configpath,
        service=self.id,
        service_id=db_game["appid"],
    )
    return game_id
wipe_game_cache(self)
Source code in lutris/services/base.py
def wipe_game_cache(self):
    logger.debug("Deleting games from service-games for %s", self.id)
    sql.db_delete(PGA_DB, "service_games", "service", self.id)

LutrisBanner (ServiceMedia)

Source code in lutris/services/base.py
class LutrisBanner(ServiceMedia):
    service = 'lutris'
    size = (184, 69)
    dest_path = settings.BANNER_PATH
    file_pattern = "%s.jpg"
    api_field = 'banner_url'
api_field
dest_path
file_pattern
service
size

LutrisCoverart (ServiceMedia)

Source code in lutris/services/base.py
class LutrisCoverart(ServiceMedia):
    service = 'lutris'
    size = (264, 352)
    file_pattern = "%s.jpg"
    dest_path = settings.COVERART_PATH
    api_field = 'coverart'
api_field
dest_path
file_pattern
service
size

LutrisCoverartMedium (LutrisCoverart)

Source code in lutris/services/base.py
class LutrisCoverartMedium(LutrisCoverart):
    size = (176, 234)
size

LutrisIcon (LutrisBanner)

Source code in lutris/services/base.py
class LutrisIcon(LutrisBanner):
    size = (32, 32)
    dest_path = settings.ICON_PATH
    file_pattern = "lutris_%s.png"
    api_field = 'icon_url'
api_field
dest_path
file_pattern
size

OnlineService (BaseService)

Base class for online gaming services

Source code in lutris/services/base.py
class OnlineService(BaseService):
    """Base class for online gaming services"""

    online = True
    cookies_path = NotImplemented
    cache_path = NotImplemented
    requires_login_page = False

    @property
    def credential_files(self):
        """Return a list of all files used for authentication"""
        return [self.cookies_path]

    def login(self, parent=None):
        logger.debug("Connecting to %s", self.name)
        dialog = WebConnectDialog(self, parent)
        dialog.set_modal(True)
        dialog.show()

    def is_authenticated(self):
        """Return whether the service is authenticated"""
        return all(system.path_exists(path) for path in self.credential_files)

    def wipe_game_cache(self):
        """Wipe the game cache, allowing it to be reloaded"""
        if self.cache_path:
            logger.debug("Deleting %s cache %s", self.id, self.cache_path)
            if os.path.isdir(self.cache_path):
                shutil.rmtree(self.cache_path)
            elif system.path_exists(self.cache_path):
                os.remove(self.cache_path)
        super().wipe_game_cache()

    def logout(self):
        """Disconnect from the service by removing all credentials"""
        self.wipe_game_cache()
        for auth_file in self.credential_files:
            try:
                os.remove(auth_file)
            except OSError:
                logger.warning("Unable to remove %s", auth_file)
        logger.debug("logged out from %s", self.id)
        self.emit("service-logout")

    def load_cookies(self):
        """Load cookies from disk"""
        if not system.path_exists(self.cookies_path):
            logger.warning("No cookies found in %s, please authenticate first", self.cookies_path)
            return
        cookiejar = WebkitCookieJar(self.cookies_path)
        cookiejar.load()
        return cookiejar
cache_path
cookies_path
credential_files property readonly

Return a list of all files used for authentication

online
requires_login_page
is_authenticated(self)

Return whether the service is authenticated

Source code in lutris/services/base.py
def is_authenticated(self):
    """Return whether the service is authenticated"""
    return all(system.path_exists(path) for path in self.credential_files)
load_cookies(self)

Load cookies from disk

Source code in lutris/services/base.py
def load_cookies(self):
    """Load cookies from disk"""
    if not system.path_exists(self.cookies_path):
        logger.warning("No cookies found in %s, please authenticate first", self.cookies_path)
        return
    cookiejar = WebkitCookieJar(self.cookies_path)
    cookiejar.load()
    return cookiejar
login(self, parent=None)
Source code in lutris/services/base.py
def login(self, parent=None):
    logger.debug("Connecting to %s", self.name)
    dialog = WebConnectDialog(self, parent)
    dialog.set_modal(True)
    dialog.show()
logout(self)

Disconnect from the service by removing all credentials

Source code in lutris/services/base.py
def logout(self):
    """Disconnect from the service by removing all credentials"""
    self.wipe_game_cache()
    for auth_file in self.credential_files:
        try:
            os.remove(auth_file)
        except OSError:
            logger.warning("Unable to remove %s", auth_file)
    logger.debug("logged out from %s", self.id)
    self.emit("service-logout")
wipe_game_cache(self)

Wipe the game cache, allowing it to be reloaded

Source code in lutris/services/base.py
def wipe_game_cache(self):
    """Wipe the game cache, allowing it to be reloaded"""
    if self.cache_path:
        logger.debug("Deleting %s cache %s", self.id, self.cache_path)
        if os.path.isdir(self.cache_path):
            shutil.rmtree(self.cache_path)
        elif system.path_exists(self.cache_path):
            os.remove(self.cache_path)
    super().wipe_game_cache()

battlenet

Battle.net service. Not ready yet.

BattleNetService (OnlineService)

Service class for Battle.net

Source code in lutris/services/battlenet.py
class BattleNetService(OnlineService):
    """Service class for Battle.net"""

    id = "battlenet"
    name = _("Battle.net")
    icon = "battlenet"
    medias = {}
    region = "na"

    @property
    def oauth_url(self):
        """Return the URL used for OAuth sign in"""
        if self.region == "cn":
            return "https://www.battlenet.com.cn/oauth"
        return "https://%s.battle.net/oauth" % self.region

    @property
    def api_url(self):
        """Main API endpoint"""
        if self.region == "cn":
            return "https://gateway.battlenet.com.cn"
        return "https://%s.api.blizzard.com" % self.region

    @property
    def login_url(self):
        """Battle.net login URL"""
        if self.region == "cn":
            return "https://www.battlenet.com.cn/login/zh"
        return "https://%s.battle.net/login/en" % self.region
api_url property readonly

Main API endpoint

icon
id
login_url property readonly

Battle.net login URL

medias
name
oauth_url property readonly

Return the URL used for OAuth sign in

region

bethesda

Bethesda service. Not ready yet.

BethesdaService (OnlineService)

Service class for Battle.net

Source code in lutris/services/bethesda.py
class BethesdaService(OnlineService):
    """Service class for Battle.net"""

    id = "bethesda"
    name = _("Bethesda")
    icon = "bethesda"
icon
id
name

dolphin

DolphinBanner (ServiceMedia)

Source code in lutris/services/dolphin.py
class DolphinBanner(ServiceMedia):
    service = "dolphin"
    source = "local"
    size = (96, 32)
    file_pattern = "%s.png"
    dest_path = os.path.join(settings.CACHE_DIR, "dolphin/banners/small")
dest_path
file_pattern
service
size
source

DolphinGame (ServiceGame)

Game for the Dolphin emulator

Source code in lutris/services/dolphin.py
class DolphinGame(ServiceGame):
    """Game for the Dolphin emulator"""

    service = "dolphin"
    runner = "dolphin"
    installer_slug = "dolphin"

    @classmethod
    def new_from_cache(cls, cache_entry):
        """Create a service game from an entry from the Dolphin cache"""
        service_game = cls()
        service_game.name = cache_entry["internal_name"]
        service_game.appid = str(cache_entry["game_id"])
        service_game.slug = slugify(cache_entry["internal_name"])
        service_game.icon = service_game.get_banner(cache_entry)

        service_game.details = json.dumps({
            "path": cache_entry["file_path"],
            "platform": cache_entry["platform"][:-1]
        })
        return service_game

    @staticmethod
    def get_game_name(cache_entry):
        names = cache_entry["long_names"]
        name_index = 1 if len(names.keys()) > 1 else 0
        return str(names[list(names.keys())[name_index]])

    def get_banner(self, cache_entry):
        banner = DolphinBanner()
        banner_path = banner.get_absolute_path(self.appid)

        if os.path.exists(banner_path):
            return banner_path

        (width, height), data = cache_entry["volume_banner"]
        if data:
            img = Image.frombytes("RGB", (width, height), data, "raw", ("BGRX"))
            # 96x32 is a bit small, maybe 2x scale?
            # img.resize((width * 2, height * 2))
            img.save(banner_path)
            return banner_path

        return ""
installer_slug
runner
service
get_banner(self, cache_entry)
Source code in lutris/services/dolphin.py
def get_banner(self, cache_entry):
    banner = DolphinBanner()
    banner_path = banner.get_absolute_path(self.appid)

    if os.path.exists(banner_path):
        return banner_path

    (width, height), data = cache_entry["volume_banner"]
    if data:
        img = Image.frombytes("RGB", (width, height), data, "raw", ("BGRX"))
        # 96x32 is a bit small, maybe 2x scale?
        # img.resize((width * 2, height * 2))
        img.save(banner_path)
        return banner_path

    return ""
get_game_name(cache_entry) staticmethod
Source code in lutris/services/dolphin.py
@staticmethod
def get_game_name(cache_entry):
    names = cache_entry["long_names"]
    name_index = 1 if len(names.keys()) > 1 else 0
    return str(names[list(names.keys())[name_index]])
new_from_cache(cache_entry) classmethod

Create a service game from an entry from the Dolphin cache

Source code in lutris/services/dolphin.py
@classmethod
def new_from_cache(cls, cache_entry):
    """Create a service game from an entry from the Dolphin cache"""
    service_game = cls()
    service_game.name = cache_entry["internal_name"]
    service_game.appid = str(cache_entry["game_id"])
    service_game.slug = slugify(cache_entry["internal_name"])
    service_game.icon = service_game.get_banner(cache_entry)

    service_game.details = json.dumps({
        "path": cache_entry["file_path"],
        "platform": cache_entry["platform"][:-1]
    })
    return service_game

DolphinService (BaseService)

Source code in lutris/services/dolphin.py
class DolphinService(BaseService):
    id = "dolphin"
    icon = "dolphin"
    name = _("Dolphin")
    local = True
    medias = {
        "icon": DolphinBanner
    }

    def load(self):
        if not system.path_exists(DOLPHIN_GAME_CACHE_FILE):
            return
        cache_reader = DolphinCacheReader()
        dolphin_games = [DolphinGame.new_from_cache(game) for game in cache_reader.get_games()]
        for game in dolphin_games:
            game.save()
        return dolphin_games

    def generate_installer(self, db_game):
        details = json.loads(db_game["details"])
        return {
            "name": db_game["name"],
            "version": "Dolphin",
            "slug": db_game["slug"],
            "game_slug": slugify(db_game["name"]),
            "runner": "dolphin",
            "script": {
                "game": {
                    "main_file": details["path"],
                    "platform": details["platform"]
                },
            }
        }

    def get_game_directory(self, installer):
        """Pull install location from installer"""
        return os.path.dirname(installer["script"]["game"]["main_file"])
icon
id
local
medias
name
generate_installer(self, db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/dolphin.py
def generate_installer(self, db_game):
    details = json.loads(db_game["details"])
    return {
        "name": db_game["name"],
        "version": "Dolphin",
        "slug": db_game["slug"],
        "game_slug": slugify(db_game["name"]),
        "runner": "dolphin",
        "script": {
            "game": {
                "main_file": details["path"],
                "platform": details["platform"]
            },
        }
    }
get_game_directory(self, installer)

Pull install location from installer

Source code in lutris/services/dolphin.py
def get_game_directory(self, installer):
    """Pull install location from installer"""
    return os.path.dirname(installer["script"]["game"]["main_file"])
load(self)
Source code in lutris/services/dolphin.py
def load(self):
    if not system.path_exists(DOLPHIN_GAME_CACHE_FILE):
        return
    cache_reader = DolphinCacheReader()
    dolphin_games = [DolphinGame.new_from_cache(game) for game in cache_reader.get_games()]
    for game in dolphin_games:
        game.save()
    return dolphin_games

egs

Epic Games Store service

BANNER_SIZE

BOX_ART_SIZE

EGS_BANNERS_PATH

EGS_BOX_ART_PATH

EGS_GAME_ART_PATH

EGS_GAME_BOX_PATH

EGS_LOGO_PATH

DieselGameBannerSmall (DieselGameBox)

Source code in lutris/services/egs.py
class DieselGameBannerSmall(DieselGameBox):
    size = (158, 89)
    remote_size = (316, 178)
remote_size
size

DieselGameBox (DieselGameBoxTall)

EGS game box

Source code in lutris/services/egs.py
class DieselGameBox(DieselGameBoxTall):
    """EGS game box"""
    size = (316, 178)
    remote_size = size
    min_logo_x = 300
    min_logo_y = 150
    dest_path = os.path.join(settings.CACHE_DIR, "egs/game_box")
    api_field = "DieselGameBox"
api_field
dest_path
min_logo_x
min_logo_y
remote_size
size

EGS game box

Source code in lutris/services/egs.py
class DieselGameBoxLogo(DieselGameMedia):
    """EGS game box"""
    size = (200, 100)
    remote_size = size
    file_pattern = "%s.png"
    visible = False
    dest_path = os.path.join(settings.CACHE_DIR, "egs/game_logo")
    api_field = "DieselGameBoxLogo"
api_field
dest_path
file_pattern
remote_size
size
visible

DieselGameBoxSmall (DieselGameBoxTall)

Source code in lutris/services/egs.py
class DieselGameBoxSmall(DieselGameBoxTall):
    size = (100, 133)
    remote_size = (200, 267)
remote_size
size

DieselGameBoxTall (DieselGameMedia)

EGS tall game box

Source code in lutris/services/egs.py
class DieselGameBoxTall(DieselGameMedia):
    """EGS tall game box"""
    size = (200, 267)
    remote_size = size
    min_logo_x = 100
    min_logo_y = 100
    dest_path = os.path.join(settings.CACHE_DIR, "egs/game_box_tall")
    api_field = "DieselGameBoxTall"

    def render(self):
        for filename in os.listdir(self.dest_path):
            self._render_filename(filename)
api_field
dest_path
min_logo_x
min_logo_y
remote_size
size
render(self)

Used if the media requires extra processing

Source code in lutris/services/egs.py
def render(self):
    for filename in os.listdir(self.dest_path):
        self._render_filename(filename)

DieselGameMedia (ServiceMedia)

Source code in lutris/services/egs.py
class DieselGameMedia(ServiceMedia):
    service = "egs"
    remote_size = (200, 267)
    file_pattern = "%s.jpg"
    min_logo_x = 300
    min_logo_y = 150

    def _render_filename(self, filename):
        game_box_path = os.path.join(self.dest_path, filename)
        logo_path = os.path.join(EGS_LOGO_PATH, filename.replace(".jpg", ".png"))
        has_logo = os.path.exists(logo_path)
        thumb_image = Image.open(game_box_path)
        thumb_image = thumb_image.convert("RGBA")
        thumb_image = thumbnail_image(thumb_image, self.remote_size)
        if has_logo:
            logo_image = Image.open(logo_path)
            logo_image = logo_image.convert("RGBA")
            logo_width, logo_height = logo_image.size
            if logo_width > self.min_logo_x:
                logo_image = logo_image.resize((self.min_logo_x, int(
                    logo_height * (self.min_logo_x / logo_width))), resample=Image.BICUBIC)
            elif logo_height > self.min_logo_y:
                logo_image = logo_image.resize(
                    (int(logo_width * (self.min_logo_y / logo_height)), self.min_logo_y), resample=Image.BICUBIC)
            thumb_image = paste_overlay(thumb_image, logo_image)
        thumb_path = os.path.join(self.dest_path, filename)
        thumb_image = thumb_image.convert("RGB")
        thumb_image.save(thumb_path)

    def get_media_url(self, details):
        for image in details.get("keyImages", []):
            if image["type"] == self.api_field:
                return image["url"] + "?w=%s&resize=1&h=%s" % (
                    self.remote_size[0],
                    self.remote_size[1]
                )
file_pattern
min_logo_x
min_logo_y
remote_size
service
get_media_url(self, details)
Source code in lutris/services/egs.py
def get_media_url(self, details):
    for image in details.get("keyImages", []):
        if image["type"] == self.api_field:
            return image["url"] + "?w=%s&resize=1&h=%s" % (
                self.remote_size[0],
                self.remote_size[1]
            )

EGSGame (ServiceGame)

Service game for Epic Games Store

Source code in lutris/services/egs.py
class EGSGame(ServiceGame):
    """Service game for Epic Games Store"""
    service = "egs"

    @classmethod
    def new_from_api(cls, egs_game):
        """Convert an EGS game to a service game"""
        service_game = cls()
        service_game.appid = egs_game["appName"]
        service_game.slug = slugify(egs_game["title"])
        service_game.name = egs_game["title"]
        service_game.details = json.dumps(egs_game)
        return service_game
service
new_from_api(egs_game) classmethod

Convert an EGS game to a service game

Source code in lutris/services/egs.py
@classmethod
def new_from_api(cls, egs_game):
    """Convert an EGS game to a service game"""
    service_game = cls()
    service_game.appid = egs_game["appName"]
    service_game.slug = slugify(egs_game["title"])
    service_game.name = egs_game["title"]
    service_game.details = json.dumps(egs_game)
    return service_game

EpicGamesStoreService (OnlineService)

Service class for Epic Games Store

Source code in lutris/services/egs.py
class EpicGamesStoreService(OnlineService):
    """Service class for Epic Games Store"""

    id = "egs"
    name = _("Epic Games Store")
    icon = "egs"
    online = True
    runner = "wine"
    client_installer = "epic-games-store"
    medias = {
        "game_box_small": DieselGameBoxSmall,
        "game_banner_small": DieselGameBannerSmall,
        "game_box": DieselGameBox,
        "box_tall": DieselGameBoxTall,
    }
    extra_medias = {
        "logo": DieselGameBoxLogo,
    }
    default_format = "game_banner_small"
    requires_login_page = True
    cookies_path = os.path.join(settings.CACHE_DIR, ".egs.auth")
    token_path = os.path.join(settings.CACHE_DIR, ".egs.token")
    cache_path = os.path.join(settings.CACHE_DIR, "egs-library.json")
    login_url = "https://www.epicgames.com/id/login?redirectUrl=https://www.epicgames.com/id/api/redirect"
    redirect_uri = "https://www.epicgames.com/id/api/redirect"
    oauth_url = 'https://account-public-service-prod03.ol.epicgames.com'
    catalog_url = 'https://catalog-public-service-prod06.ol.epicgames.com'
    library_url = 'https://library-service.live.use1a.on.epicgames.com'
    is_loading = False

    user_agent = (
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
        'AppleWebKit/537.36 (KHTML, like Gecko) '
        'EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live '
        'UnrealEngine/4.23.0-14907503+++Portal+Release-Live '
        'Chrome/84.0.4147.38 Safari/537.36'
    )

    def __init__(self):
        super().__init__()
        self.session = requests.session()
        self.session.headers['User-Agent'] = self.user_agent
        if os.path.exists(self.token_path):
            with open(self.token_path, encoding='utf-8') as token_file:
                self.session_data = json.loads(token_file.read())
        else:
            self.session_data = {}

    @property
    def http_basic_auth(self):
        return requests.auth.HTTPBasicAuth(
            '34a02cf8f4414e29b15921876da36f9a',
            'daafbccc737745039dffe53d94fc76cf'
        )

    def run(self):
        egs = get_game_by_field(self.client_installer, "slug")
        egs_game = Game(egs["id"])
        egs_game.emit("game-launch")

    def is_launchable(self):
        return get_game_by_field(self.client_installer, "slug")

    def is_connected(self):
        return self.is_authenticated()

    def login_callback(self, content):
        """Once the user logs in in a browser window, Epic redirects
        to a page containing a Session ID which we can use to finish the authentication.
        Store session ID and exchange token to auth file"""
        logger.debug("Login to EGS successful")
        logger.debug(content)
        content_json = json.loads(content.decode())
        session_id = content_json["sid"]
        _session = requests.session()
        _session.headers.update({
            'X-Epic-Event-Action': 'login',
            'X-Epic-Event-Category': 'login',
            'X-Epic-Strategy-Flags': '',
            'X-Requested-With': 'XMLHttpRequest',
            'User-Agent': self.user_agent
        })

        _session.get('https://www.epicgames.com/id/api/set-sid', params={'sid': session_id})
        _session.get('https://www.epicgames.com/id/api/csrf')
        response = _session.post(
            'https://www.epicgames.com/id/api/exchange/generate',
            headers={'X-XSRF-TOKEN': _session.cookies['XSRF-TOKEN']}
        )

        if response.status_code != 200:
            logger.error("Failed to connec to EGS (Status %s): %s", response.status_code, response.json())
            return

        self.start_session(response.json()['code'])
        self.emit("service-login")

    def resume_session(self):
        self.session.headers['Authorization'] = 'bearer %s' % self.session_data["access_token"]
        response = self.session.get('%s/account/api/oauth/verify' % self.oauth_url)
        if response.status_code >= 500:
            response.raise_for_status()

        response_content = response.json()
        if 'errorMessage' in response_content:
            raise RuntimeError(response_content)
        return response_content

    def start_session(self, exchange_code=None):
        if exchange_code:
            grant_type = 'exchange_code'
            token = exchange_code
        else:
            grant_type = 'refresh_token'
            token = self.session_data["refresh_token"]

        response = self.session.post(
            'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token',
            data={
                'grant_type': grant_type,
                grant_type: token,
                'token_type': 'eg1'
            },
            auth=self.http_basic_auth
        )
        if response.status_code >= 500:
            response.raise_for_status()

        response_content = response.json()
        if 'error' in response_content:
            raise RuntimeError(response_content)
        with open(self.token_path, "w", encoding='utf-8') as auth_file:
            auth_file.write(json.dumps(response_content, indent=2))
        self.session_data = response_content

    def get_game_details(self, asset):
        namespace = asset["namespace"]
        catalog_item_id = asset["catalogItemId"]
        response = self.session.get(
            '%s/catalog/api/shared/namespace/%s/bulk/items' % (self.catalog_url, namespace),
            params={
                "id": catalog_item_id,
                "includeDLCDetails": True,
                "includeMainGameDetails": True,
                "country": "US",
                "locale": "en"
            }
        )
        response.raise_for_status()
        # Merge the details with the initial asset to keep 'appName'
        asset.update(response.json()[catalog_item_id])
        return asset

    def get_library(self):
        self.resume_session()
        response = self.session.get(
            '%s/library/api/public/items' % self.library_url,
            params={'includeMetadata': 'true'}
        )
        response.raise_for_status()
        resData = response.json()
        records = resData['records']
        cursor = resData['responseMetadata'].get('nextCursor', None)
        while cursor:
            response = self.session.get(
                '%s/library/api/public/items' % self.library_url,
                params={'includeMetadata': 'true',
                        'cursor': cursor}
            )
            response.raise_for_status()
            resData = response.json()
            records.extend(resData['records'])
            cursor = resData['responseMetadata'].get('nextCursor', None)

        games = []
        for record in records:
            if record["namespace"] == "ue":
                continue
            game_details = self.get_game_details(record)
            games.append(game_details)
        return games

    def load(self):
        """Load the list of games"""
        if self.is_loading:
            logger.warning("EGS games are already loading")
            return
        self.is_loading = True
        try:
            library = self.get_library()
        except Exception as ex:  # pylint=disable:broad-except
            self.is_loading = False
            logger.warning("EGS Token expired")
            raise AuthTokenExpired from ex
        egs_games = []
        for game in library:
            egs_game = EGSGame.new_from_api(game)
            egs_game.save()
            egs_games.append(egs_game)
        self.is_loading = False
        return egs_games

    def install_from_egs(self, egs_game, manifest):
        """Create a new Lutris game based on an existing EGS install"""
        app_name = manifest["AppName"]
        logger.debug("Installing EGS game %s", app_name)
        service_game = ServiceGameCollection.get_game("egs", app_name)
        if not service_game:
            logger.error("Aborting install, %s is not present in the game library.", app_name)
            return
        lutris_game_id = slugify(service_game["name"]) + "-" + self.id
        existing_game = get_game_by_field(lutris_game_id, "installer_slug")
        if existing_game:
            return
        game_config = LutrisConfig(game_config_id=egs_game["configpath"]).game_level
        game_config["game"]["args"] = get_launch_arguments(app_name)
        configpath = write_game_config(lutris_game_id, game_config)
        game_id = add_game(
            name=service_game["name"],
            runner=egs_game["runner"],
            slug=slugify(service_game["name"]),
            directory=egs_game["directory"],
            installed=1,
            installer_slug=lutris_game_id,
            configpath=configpath,
            service=self.id,
            service_id=app_name,
        )
        return game_id

    def add_installed_games(self):
        """Scan an existing EGS install for games"""
        egs_game = get_game_by_field("epic-games-store", "slug")
        if not egs_game:
            logger.error("EGS is not installed in Lutris")
            return

        egs_prefix = egs_game["directory"].split("drive_c")[0]
        logger.info("EGS detected in %s", egs_prefix)
        if not system.path_exists(os.path.join(egs_prefix, "drive_c")):
            logger.error("Invalid install of EGS at %s", egs_prefix)
            return
        egs_launcher = EGSLauncher(egs_prefix)
        for manifest in egs_launcher.iter_manifests():
            self.install_from_egs(egs_game, manifest)
        logger.debug("All EGS games imported")

    def generate_installer(self, db_game, egs_db_game):
        egs_game = Game(egs_db_game["id"])
        egs_exe = egs_game.config.game_config["exe"]
        if not os.path.isabs(egs_exe):
            egs_exe = os.path.join(egs_game.config.game_config["prefix"], egs_exe)
        return {
            "name": db_game["name"],
            "version": self.name,
            "slug": slugify(db_game["name"]) + "-" + self.id,
            "game_slug": slugify(db_game["name"]),
            "runner": self.runner,
            "appid": db_game["appid"],
            "script": {
                "requires": self.client_installer,
                "game": {
                    "args": get_launch_arguments(db_game["appid"]),
                },
                "installer": [
                    {"task": {
                        "name": "wineexec",
                        "executable": egs_exe,
                        "args": get_launch_arguments(db_game["appid"], "install"),
                        "prefix": egs_game.config.game_config["prefix"],
                        "description": (
                            "The Epic Game Store will now open. Please launch "
                            "the installation of %s then close the EGS client "
                            "once the game has been downloaded." % db_game["name"]
                        )
                    }}
                ]
            }
        }

    def install(self, db_game):
        egs_game = get_game_by_field(self.client_installer, "slug")
        application = Gio.Application.get_default()
        if not egs_game or not egs_game["installed"]:
            logger.warning("EGS (%s) not installed", self.client_installer)
            installers = get_installers(
                game_slug=self.client_installer,
            )
            application.show_installer_window(installers)
        else:
            application.show_installer_window(
                [self.generate_installer(db_game, egs_game)],
                service=self,
                appid=db_game["appid"]
            )
cache_path
catalog_url
client_installer
cookies_path
default_format
extra_medias
http_basic_auth property readonly
icon
id
is_loading
library_url
login_url
medias
name
oauth_url
online
redirect_uri
requires_login_page
runner
token_path
user_agent
__init__(self) special
Source code in lutris/services/egs.py
def __init__(self):
    super().__init__()
    self.session = requests.session()
    self.session.headers['User-Agent'] = self.user_agent
    if os.path.exists(self.token_path):
        with open(self.token_path, encoding='utf-8') as token_file:
            self.session_data = json.loads(token_file.read())
    else:
        self.session_data = {}
add_installed_games(self)

Scan an existing EGS install for games

Source code in lutris/services/egs.py
def add_installed_games(self):
    """Scan an existing EGS install for games"""
    egs_game = get_game_by_field("epic-games-store", "slug")
    if not egs_game:
        logger.error("EGS is not installed in Lutris")
        return

    egs_prefix = egs_game["directory"].split("drive_c")[0]
    logger.info("EGS detected in %s", egs_prefix)
    if not system.path_exists(os.path.join(egs_prefix, "drive_c")):
        logger.error("Invalid install of EGS at %s", egs_prefix)
        return
    egs_launcher = EGSLauncher(egs_prefix)
    for manifest in egs_launcher.iter_manifests():
        self.install_from_egs(egs_game, manifest)
    logger.debug("All EGS games imported")
generate_installer(self, db_game, egs_db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/egs.py
def generate_installer(self, db_game, egs_db_game):
    egs_game = Game(egs_db_game["id"])
    egs_exe = egs_game.config.game_config["exe"]
    if not os.path.isabs(egs_exe):
        egs_exe = os.path.join(egs_game.config.game_config["prefix"], egs_exe)
    return {
        "name": db_game["name"],
        "version": self.name,
        "slug": slugify(db_game["name"]) + "-" + self.id,
        "game_slug": slugify(db_game["name"]),
        "runner": self.runner,
        "appid": db_game["appid"],
        "script": {
            "requires": self.client_installer,
            "game": {
                "args": get_launch_arguments(db_game["appid"]),
            },
            "installer": [
                {"task": {
                    "name": "wineexec",
                    "executable": egs_exe,
                    "args": get_launch_arguments(db_game["appid"], "install"),
                    "prefix": egs_game.config.game_config["prefix"],
                    "description": (
                        "The Epic Game Store will now open. Please launch "
                        "the installation of %s then close the EGS client "
                        "once the game has been downloaded." % db_game["name"]
                    )
                }}
            ]
        }
    }
get_game_details(self, asset)
Source code in lutris/services/egs.py
def get_game_details(self, asset):
    namespace = asset["namespace"]
    catalog_item_id = asset["catalogItemId"]
    response = self.session.get(
        '%s/catalog/api/shared/namespace/%s/bulk/items' % (self.catalog_url, namespace),
        params={
            "id": catalog_item_id,
            "includeDLCDetails": True,
            "includeMainGameDetails": True,
            "country": "US",
            "locale": "en"
        }
    )
    response.raise_for_status()
    # Merge the details with the initial asset to keep 'appName'
    asset.update(response.json()[catalog_item_id])
    return asset
get_library(self)
Source code in lutris/services/egs.py
def get_library(self):
    self.resume_session()
    response = self.session.get(
        '%s/library/api/public/items' % self.library_url,
        params={'includeMetadata': 'true'}
    )
    response.raise_for_status()
    resData = response.json()
    records = resData['records']
    cursor = resData['responseMetadata'].get('nextCursor', None)
    while cursor:
        response = self.session.get(
            '%s/library/api/public/items' % self.library_url,
            params={'includeMetadata': 'true',
                    'cursor': cursor}
        )
        response.raise_for_status()
        resData = response.json()
        records.extend(resData['records'])
        cursor = resData['responseMetadata'].get('nextCursor', None)

    games = []
    for record in records:
        if record["namespace"] == "ue":
            continue
        game_details = self.get_game_details(record)
        games.append(game_details)
    return games
install(self, db_game)

Install a service game, or starts the installer of the game.

Parameters:

Name Type Description Default
db_game dict or str

Database fields of the game to add, or (for Lutris service only the slug of the game.)

required

Returns:

Type Description
str

The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None.

Source code in lutris/services/egs.py
def install(self, db_game):
    egs_game = get_game_by_field(self.client_installer, "slug")
    application = Gio.Application.get_default()
    if not egs_game or not egs_game["installed"]:
        logger.warning("EGS (%s) not installed", self.client_installer)
        installers = get_installers(
            game_slug=self.client_installer,
        )
        application.show_installer_window(installers)
    else:
        application.show_installer_window(
            [self.generate_installer(db_game, egs_game)],
            service=self,
            appid=db_game["appid"]
        )
install_from_egs(self, egs_game, manifest)

Create a new Lutris game based on an existing EGS install

Source code in lutris/services/egs.py
def install_from_egs(self, egs_game, manifest):
    """Create a new Lutris game based on an existing EGS install"""
    app_name = manifest["AppName"]
    logger.debug("Installing EGS game %s", app_name)
    service_game = ServiceGameCollection.get_game("egs", app_name)
    if not service_game:
        logger.error("Aborting install, %s is not present in the game library.", app_name)
        return
    lutris_game_id = slugify(service_game["name"]) + "-" + self.id
    existing_game = get_game_by_field(lutris_game_id, "installer_slug")
    if existing_game:
        return
    game_config = LutrisConfig(game_config_id=egs_game["configpath"]).game_level
    game_config["game"]["args"] = get_launch_arguments(app_name)
    configpath = write_game_config(lutris_game_id, game_config)
    game_id = add_game(
        name=service_game["name"],
        runner=egs_game["runner"],
        slug=slugify(service_game["name"]),
        directory=egs_game["directory"],
        installed=1,
        installer_slug=lutris_game_id,
        configpath=configpath,
        service=self.id,
        service_id=app_name,
    )
    return game_id
is_connected(self)
Source code in lutris/services/egs.py
def is_connected(self):
    return self.is_authenticated()
is_launchable(self)
Source code in lutris/services/egs.py
def is_launchable(self):
    return get_game_by_field(self.client_installer, "slug")
load(self)

Load the list of games

Source code in lutris/services/egs.py
def load(self):
    """Load the list of games"""
    if self.is_loading:
        logger.warning("EGS games are already loading")
        return
    self.is_loading = True
    try:
        library = self.get_library()
    except Exception as ex:  # pylint=disable:broad-except
        self.is_loading = False
        logger.warning("EGS Token expired")
        raise AuthTokenExpired from ex
    egs_games = []
    for game in library:
        egs_game = EGSGame.new_from_api(game)
        egs_game.save()
        egs_games.append(egs_game)
    self.is_loading = False
    return egs_games
login_callback(self, content)

Once the user logs in in a browser window, Epic redirects to a page containing a Session ID which we can use to finish the authentication. Store session ID and exchange token to auth file

Source code in lutris/services/egs.py
def login_callback(self, content):
    """Once the user logs in in a browser window, Epic redirects
    to a page containing a Session ID which we can use to finish the authentication.
    Store session ID and exchange token to auth file"""
    logger.debug("Login to EGS successful")
    logger.debug(content)
    content_json = json.loads(content.decode())
    session_id = content_json["sid"]
    _session = requests.session()
    _session.headers.update({
        'X-Epic-Event-Action': 'login',
        'X-Epic-Event-Category': 'login',
        'X-Epic-Strategy-Flags': '',
        'X-Requested-With': 'XMLHttpRequest',
        'User-Agent': self.user_agent
    })

    _session.get('https://www.epicgames.com/id/api/set-sid', params={'sid': session_id})
    _session.get('https://www.epicgames.com/id/api/csrf')
    response = _session.post(
        'https://www.epicgames.com/id/api/exchange/generate',
        headers={'X-XSRF-TOKEN': _session.cookies['XSRF-TOKEN']}
    )

    if response.status_code != 200:
        logger.error("Failed to connec to EGS (Status %s): %s", response.status_code, response.json())
        return

    self.start_session(response.json()['code'])
    self.emit("service-login")
resume_session(self)
Source code in lutris/services/egs.py
def resume_session(self):
    self.session.headers['Authorization'] = 'bearer %s' % self.session_data["access_token"]
    response = self.session.get('%s/account/api/oauth/verify' % self.oauth_url)
    if response.status_code >= 500:
        response.raise_for_status()

    response_content = response.json()
    if 'errorMessage' in response_content:
        raise RuntimeError(response_content)
    return response_content
run(self)

Override this method to run a launcher

Source code in lutris/services/egs.py
def run(self):
    egs = get_game_by_field(self.client_installer, "slug")
    egs_game = Game(egs["id"])
    egs_game.emit("game-launch")
start_session(self, exchange_code=None)
Source code in lutris/services/egs.py
def start_session(self, exchange_code=None):
    if exchange_code:
        grant_type = 'exchange_code'
        token = exchange_code
    else:
        grant_type = 'refresh_token'
        token = self.session_data["refresh_token"]

    response = self.session.post(
        'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token',
        data={
            'grant_type': grant_type,
            grant_type: token,
            'token_type': 'eg1'
        },
        auth=self.http_basic_auth
    )
    if response.status_code >= 500:
        response.raise_for_status()

    response_content = response.json()
    if 'error' in response_content:
        raise RuntimeError(response_content)
    with open(self.token_path, "w", encoding='utf-8') as auth_file:
        auth_file.write(json.dumps(response_content, indent=2))
    self.session_data = response_content

get_launch_arguments(app_name, action='launch')

Source code in lutris/services/egs.py
def get_launch_arguments(app_name, action="launch"):
    return (
        "-opengl"
        " -SkipBuildPatchPrereq"
        " -com.epicgames.launcher://apps/%s?action=%s"
    ) % (app_name, action)

gog

Module for handling the GOG service

GOGGame (ServiceGame)

Representation of a GOG game

Source code in lutris/services/gog.py
class GOGGame(ServiceGame):

    """Representation of a GOG game"""
    service = "gog"

    @classmethod
    def new_from_gog_game(cls, gog_game):
        """Return a GOG game instance from the API info"""
        service_game = GOGGame()
        service_game.appid = str(gog_game["id"])
        service_game.slug = gog_game["slug"]
        service_game.name = gog_game["title"]
        service_game.details = json.dumps(gog_game)
        return service_game
service
new_from_gog_game(gog_game) classmethod

Return a GOG game instance from the API info

Source code in lutris/services/gog.py
@classmethod
def new_from_gog_game(cls, gog_game):
    """Return a GOG game instance from the API info"""
    service_game = GOGGame()
    service_game.appid = str(gog_game["id"])
    service_game.slug = gog_game["slug"]
    service_game.name = gog_game["title"]
    service_game.details = json.dumps(gog_game)
    return service_game

GOGService (OnlineService)

Service class for GOG

Source code in lutris/services/gog.py
class GOGService(OnlineService):
    """Service class for GOG"""

    id = "gog"
    name = _("GOG")
    icon = "gog"
    has_extras = True
    drm_free = True
    medias = {
        "banner_small": GogSmallBanner,
        "banner": GogMediumBanner,
        "banner_large": GogLargeBanner
    }
    default_format = "banner"

    embed_url = "https://embed.gog.com"
    api_url = "https://api.gog.com"

    client_id = "46899977096215655"
    client_secret = "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9"
    redirect_uri = "https://embed.gog.com/on_login_success?origin=client"

    login_success_url = "https://www.gog.com/on_login_success"
    cookies_path = os.path.join(settings.CACHE_DIR, ".gog.auth")
    token_path = os.path.join(settings.CACHE_DIR, ".gog.token")
    cache_path = os.path.join(settings.CACHE_DIR, "gog-library.json")

    is_loading = False

    def __init__(self):
        super().__init__()
        self.selected_extras = None

        gog_locales = {
            "en": "en-US",
            "de": "de-DE",
            "fr": "fr-FR",
            "pl": "pl-PL",
            "ru": "ru-RU",
            "zh": "zh-Hans",
        }
        self.locale = gog_locales.get(i18n.get_lang(), "en-US")

    @property
    def login_url(self):
        """Return authentication URL"""
        params = {
            "client_id": self.client_id,
            "layout": "client2",
            "redirect_uri": self.redirect_uri,
            "response_type": "code",
        }
        return "https://auth.gog.com/auth?" + urlencode(params)

    @property
    def credential_files(self):
        return [self.cookies_path, self.token_path]

    def is_connected(self):
        """Return whether the user is authenticated and if the service is available"""
        if not self.is_authenticated():
            return False
        try:
            user_data = self.get_user_data()
        except UnauthorizedAccess:
            logger.warning("GOG token is invalid")
            return False
        return user_data and "username" in user_data

    def load(self):
        """Load the user game library from the GOG API"""
        if self.is_loading:
            logger.warning("GOG games are already loading")
            return
        if not self.is_connected():
            logger.error("User not connected to GOG")
            return
        self.is_loading = True
        games = [GOGGame.new_from_gog_game(game) for game in self.get_library()]
        for game in games:
            game.save()
        self.match_games()
        self.is_loading = False
        return games

    def login_callback(self, url):
        return self.request_token(url)

    def request_token(self, url="", refresh_token=""):
        """Get authentication token from GOG"""
        if refresh_token:
            grant_type = "refresh_token"
            extra_params = {"refresh_token": refresh_token}
        else:
            grant_type = "authorization_code"
            parsed_url = urlparse(url)
            response_params = dict(parse_qsl(parsed_url.query))
            if "code" not in response_params:
                logger.error("code not received from GOG")
                logger.error(response_params)
                return
            extra_params = {
                "code": response_params["code"],
                "redirect_uri": self.redirect_uri,
            }

        params = {
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "grant_type": grant_type,
        }
        params.update(extra_params)
        url = "https://auth.gog.com/token?" + urlencode(params)
        request = Request(url)
        try:
            request.get()
        except HTTPError:
            logger.error("Failed to get token, check your GOG credentials.")
            logger.warning("Clearing existing credentials")
            self.logout()
            return

        token = request.json
        with open(self.token_path, "w", encoding='utf-8') as token_file:
            token_file.write(json.dumps(token))
        if not refresh_token:
            self.emit("service-login")

    def load_token(self):
        """Load token from disk"""
        if not os.path.exists(self.token_path):
            raise AuthenticationError("No GOG token available")
        with open(self.token_path, encoding='utf-8') as token_file:
            token_content = json.loads(token_file.read())
        return token_content

    def get_token_age(self):
        """Return age of token"""
        token_stat = os.stat(self.token_path)
        token_modified = token_stat.st_mtime
        return time.time() - token_modified

    def make_request(self, url):
        """Send a cookie authenticated HTTP request to GOG"""
        request = Request(url, cookies=self.load_cookies())
        request.get()
        if request.content.startswith(b"<"):
            raise AuthenticationError("Token expired, please log in again")
        return request.json

    def make_api_request(self, url):
        """Send a token authenticated request to GOG"""
        try:
            token = self.load_token()
        except AuthenticationError:
            return
        if self.get_token_age() > 2600:
            self.request_token(refresh_token=token["refresh_token"])
            token = self.load_token()
            if not token:
                logger.warning(
                    "Request to %s cancelled because the GOG token could not be acquired",
                    url,
                )
                return
        headers = {"Authorization": "Bearer " + token["access_token"]}
        request = Request(url, headers=headers, cookies=self.load_cookies())
        try:
            request.get()
        except HTTPError:
            logger.error(
                "Failed to request %s, check your GOG credentials and internet connectivity",
                url,
            )
            return
        return request.json

    def get_user_data(self):
        """Return GOG profile information"""
        url = "https://embed.gog.com/userData.json"
        return self.make_api_request(url)

    def get_library(self):
        """Return the user's library of GOG games"""
        if system.path_exists(self.cache_path):
            logger.debug("Returning cached GOG library")
            with open(self.cache_path, "r", encoding='utf-8') as gog_cache:
                return json.load(gog_cache)

        total_pages = 1
        games = []
        page = 1
        while page <= total_pages:
            products_response = self.get_products_page(page=page)
            page += 1
            total_pages = products_response["totalPages"]
            games += products_response["products"]
        with open(self.cache_path, "w", encoding='utf-8') as gog_cache:
            json.dump(games, gog_cache)
        return games

    def get_service_game(self, gog_game):
        return GOGGame.new_from_gog_game(gog_game)

    def get_products_page(self, page=1, search=None):
        """Return a single page of games"""
        if not self.is_authenticated():
            raise AuthenticationError("User is not logged in")
        params = {"mediaType": "1"}
        if page:
            params["page"] = page
        if search:
            params["search"] = search
        url = self.embed_url + "/account/getFilteredProducts?" + urlencode(params)
        return self.make_request(url)

    def get_game_dlcs(self, product_id):
        """Return the list of DLC products the user owns for a game"""
        game_details = self.get_game_details(product_id)
        if not game_details["dlcs"]:
            return []
        all_products_url = game_details["dlcs"]["expanded_all_products_url"]
        return self.make_api_request(all_products_url)

    def get_game_details(self, product_id):
        """Return game information for a given game"""
        if not product_id:
            raise ValueError("Missing product ID")
        logger.info("Getting game details for %s", product_id)
        url = "{}/products/{}?expand=downloads&locale={}".format(self.api_url, product_id, self.locale)
        return self.make_api_request(url)

    def get_download_info(self, downlink):
        """Return file download information"""
        logger.info("Getting download info for %s", downlink)
        try:
            response = self.make_api_request(downlink)
        except HTTPError as ex:
            logger.error("HTTP error: %s", ex)
            raise UnavailableGame from ex
        if not response:
            raise UnavailableGame
        for field in ("checksum", "downlink"):
            field_url = response[field]
            parsed = urlparse(field_url)
            query = dict(parse_qsl(parsed.query))
            response[field + "_filename"] = os.path.basename(query.get("path") or parsed.path)
        return response

    def get_downloads(self, gogid):
        """Return all available downloads for a GOG ID"""
        if not gogid:
            logger.warning("Unable to get GOG data because no GOG ID is available")
            return {}
        gog_data = self.get_game_details(gogid)
        if not gog_data:
            logger.warning("Unable to get GOG data for game %s", gogid)
            return {}
        return gog_data["downloads"]

    def get_extras(self, gogid):
        """Return a list of bonus content available for a GOG ID and its DLCs"""
        logger.debug("Download extras for GOG ID %s and its DLCs", gogid)
        game = self.get_game_details(gogid)
        if not game:
            logger.warning("Unable to get GOG data for game %s", gogid)
            return []
        dlcs = self.get_game_dlcs(gogid)
        products = [game, *dlcs] if dlcs else [game]
        all_extras = {}
        for product in products:
            extras = [
                {
                    "name": download.get("name", "").strip().capitalize(),
                    "type": download.get("type", "").strip(),
                    "total_size": download.get("total_size", 0),
                    "id": str(download["id"]),
                } for download in product["downloads"].get("bonus_content") or []
            ]
            if extras:
                all_extras[product.get("title", "").strip()] = extras
        return all_extras

    def get_installers(self, downloads, runner, language="en"):
        """Return available installers for a GOG game"""
        # Filter out Mac installers
        gog_installers = [installer for installer in downloads.get("installers", []) if installer["os"] != "mac"]
        available_platforms = {installer["os"] for installer in gog_installers}
        # If it's a Linux game, also filter out Windows games
        if "linux" in available_platforms:
            filter_os = "windows" if runner == "linux" else "linux"
            gog_installers = [installer for installer in gog_installers if installer["os"] != filter_os]
        return [
            installer
            for installer in gog_installers
            if installer["language"] == self.determine_language_installer(gog_installers, language)
        ]

    def get_update_versions(self, gog_id):
        """Return updates available for a game, keyed by patch version"""
        games_detail = self.get_game_details(gog_id)
        patches = games_detail["downloads"]["patches"]
        if not patches:
            logger.info("No patches for %s", games_detail)
            return {}
        patch_versions = defaultdict(list)
        for patch in patches:
            patch_versions[patch["name"]].append(patch)
        return patch_versions

    def determine_language_installer(self, gog_installers, default_language="en"):
        """Return locale language string if available in gog_installers"""
        language = i18n.get_lang()
        gog_installers = [installer for installer in gog_installers if installer["language"] == language]
        if not gog_installers:
            language = default_language
        return language

    def query_download_links(self, download):
        """Convert files from the GOG API to a format compatible with lutris installers"""
        download_links = []
        for game_file in download.get("files", []):
            downlink = game_file.get("downlink")
            if not downlink:
                logger.error("No download information for %s", game_file)
                continue
            download_info = self.get_download_info(downlink)
            for field in ('checksum', 'downlink'):
                download_links.append({
                    "name": download.get("name", ""),
                    "os": download.get("os", ""),
                    "type": download.get("type", ""),
                    "total_size": download.get("total_size", 0),
                    "id": str(game_file["id"]),
                    "url": download_info[field],
                    "filename": download_info[field + "_filename"]
                })
        return download_links

    def get_extra_files(self, downloads, installer):
        extra_files = []
        for extra in downloads["bonus_content"]:
            if str(extra["id"]) not in self.selected_extras:
                continue
            links = self.query_download_links(extra)
            for link in links:
                if link["filename"].endswith(".xml"):
                    # GOG gives a link for checksum XML files for bonus content
                    # but downloading them results in a 404 error.
                    continue
                extra_files.append(
                    InstallerFile(installer.game_slug, str(extra["id"]), {
                        "url": link["url"],
                        "filename": link["filename"],
                    })
                )
        return extra_files

    def _get_installer_links(self, installer, downloads):
        """Return links to downloadable files from a list of downloads"""
        try:
            gog_installers = self.get_installers(downloads, installer.runner)
            if not gog_installers:
                return []
            if len(gog_installers) > 1:
                logger.warning("More than 1 GOG installer found, picking first.")
            _installer = gog_installers[0]
            return self.query_download_links(_installer)
        except HTTPError as err:
            raise UnavailableGame("Couldn't load the download links for this game") from err

    def get_patch_files(self, installer, installer_file_id):
        logger.debug("Getting patches for %s", installer.version)
        downloads = self.get_downloads(installer.service_appid)
        links = []
        for patch_file in downloads["patches"]:
            if "GOG " + patch_file["version"] == installer.version:
                links += self.query_download_links(patch_file)
        return self._format_links(installer, installer_file_id, links)

    def _format_links(self, installer, installer_file_id, links):
        _installer_files = defaultdict(dict)  # keyed by filename
        for link in links:
            try:
                filename = link["filename"]
            except KeyError:
                logger.error("Invalid link: %s", link)
                raise
            if filename.lower().endswith(".xml"):
                if filename != installer_file_id:
                    filename = filename[:-4]
                _installer_files[filename]["checksum_url"] = link["url"]
                continue
            _installer_files[filename]["id"] = link["id"]
            _installer_files[filename]["url"] = link["url"]
            _installer_files[filename]["filename"] = filename
            _installer_files[filename]["total_size"] = link["total_size"]
        files = []
        file_id_provided = False  # Only assign installer_file_id once
        for _file_id in _installer_files:
            installer_file = _installer_files[_file_id]
            if "url" not in installer_file:
                raise ValueError("Invalid installer file %s" % installer_file)
            filename = installer_file["filename"]
            if filename.lower().endswith((".exe", ".sh")) and not file_id_provided:
                file_id = installer_file_id
                file_id_provided = True
            else:
                file_id = _file_id
            files.append(InstallerFile(installer.game_slug, file_id, {
                "url": installer_file["url"],
                "filename": installer_file["filename"],
                "checksum_url": installer_file.get("checksum_url")
            }))
        if not file_id_provided:
            raise UnavailableGame("Unable to determine correct file to launch installer")
        return files

    def get_installer_files(self, installer, installer_file_id):
        try:
            downloads = self.get_downloads(installer.service_appid)
        except HTTPError as err:
            raise UnavailableGame("Couldn't load the downloads for this game") from err
        links = self._get_installer_links(installer, downloads)
        if links:
            files = self._format_links(installer, installer_file_id, links)
        else:
            files = []
        if self.selected_extras:
            for extra_file in self.get_extra_files(downloads, installer):
                files.append(extra_file)
        return files

    def read_file_checksum(self, file_path):
        """Return the MD5 checksum for a GOG file
        Requires a GOG XML file as input
        This has yet to be used.
        """
        if not file_path.endswith(".xml"):
            raise ValueError("Pass a XML file to return the checksum")
        with open(file_path, encoding='utf-8') as checksum_file:
            checksum_content = checksum_file.read()
        root_elem = etree.fromstring(checksum_content)
        return (root_elem.attrib["name"], root_elem.attrib["md5"])

    def generate_installer(self, db_game):
        details = json.loads(db_game["details"])
        platforms = [platform.lower() for platform, is_supported in details["worksOn"].items() if is_supported]
        system_config = {}
        if "linux" in platforms:
            runner = "linux"
            game_config = {"exe": AUTO_ELF_EXE}
            script = [
                {"extract": {"file": "goginstaller", "format": "zip", "dst": "$CACHE"}},
                {"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR"}},
            ]
        else:
            runner = "wine"
            game_config = {"exe": AUTO_WIN32_EXE}
            script = [
                {"autosetup_gog_game": "goginstaller"},
            ]
        return {
            "name": db_game["name"],
            "version": "GOG",
            "slug": details["slug"],
            "game_slug": slugify(db_game["name"]),
            "runner": runner,
            "gogid": db_game["appid"],
            "script": {
                "game": game_config,
                "system": system_config,
                "files": [
                    {"goginstaller": "N/A:Select the installer from GOG"}
                ],
                "installer": script
            }
        }

    def get_dlc_installers(self, db_game):
        appid = db_game["service_id"]
        dlcs = self.get_game_dlcs(appid)
        installers = []
        for dlc in dlcs:
            dlc_id = "gogdlc-%s" % dlc["slug"]
            installer = {
                "name": db_game["name"],
                "version": dlc["title"],
                "slug": dlc["slug"],
                "description": "DLC for %s" % db_game["name"],
                "game_slug": slugify(db_game["name"]),
                "runner": "wine",
                "is_dlc": True,
                "dlcid": dlc["id"],
                "gogid": dlc["id"],
                "script": {
                    "extends": db_game["installer_slug"],
                    "files": [
                        {dlc_id: "N/A:Select the patch from GOG"}
                    ],
                    "installer": [
                        {"task": {"name": "wineexec", "executable": dlc_id}}
                    ]
                }
            }
            installers.append(installer)
        return installers

    def get_update_installers(self, db_game):
        appid = db_game["service_id"]
        patch_versions = self.get_update_versions(appid)
        patch_installers = []
        for version in patch_versions:
            patch = patch_versions[version]
            size = human_size(sum([part["total_size"] for part in patch]))
            patch_id = "gogpatch-%s" % slugify(patch[0]["version"])
            installer = {
                "name": db_game["name"],
                "description": patch[0]["name"] + " " + size,
                "slug": db_game["installer_slug"],
                "game_slug": db_game["slug"],
                "version": "GOG " + patch[0]["version"],
                "runner": "wine",
                "script": {
                    "extends": db_game["installer_slug"],
                    "files": [
                        {patch_id: "N/A:Select the patch from GOG"}
                    ],
                    "installer": [
                        {"task": {"name": "wineexec", "executable": patch_id}}
                    ]
                }
            }
            patch_installers.append(installer)
        return patch_installers
api_url
cache_path
client_id
client_secret
cookies_path
credential_files property readonly

Return a list of all files used for authentication

default_format
drm_free
embed_url
has_extras
icon
id
is_loading
login_success_url
login_url property readonly

Return authentication URL

medias
name
redirect_uri
token_path
__init__(self) special
Source code in lutris/services/gog.py
def __init__(self):
    super().__init__()
    self.selected_extras = None

    gog_locales = {
        "en": "en-US",
        "de": "de-DE",
        "fr": "fr-FR",
        "pl": "pl-PL",
        "ru": "ru-RU",
        "zh": "zh-Hans",
    }
    self.locale = gog_locales.get(i18n.get_lang(), "en-US")
determine_language_installer(self, gog_installers, default_language='en')

Return locale language string if available in gog_installers

Source code in lutris/services/gog.py
def determine_language_installer(self, gog_installers, default_language="en"):
    """Return locale language string if available in gog_installers"""
    language = i18n.get_lang()
    gog_installers = [installer for installer in gog_installers if installer["language"] == language]
    if not gog_installers:
        language = default_language
    return language
generate_installer(self, db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/gog.py
def generate_installer(self, db_game):
    details = json.loads(db_game["details"])
    platforms = [platform.lower() for platform, is_supported in details["worksOn"].items() if is_supported]
    system_config = {}
    if "linux" in platforms:
        runner = "linux"
        game_config = {"exe": AUTO_ELF_EXE}
        script = [
            {"extract": {"file": "goginstaller", "format": "zip", "dst": "$CACHE"}},
            {"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR"}},
        ]
    else:
        runner = "wine"
        game_config = {"exe": AUTO_WIN32_EXE}
        script = [
            {"autosetup_gog_game": "goginstaller"},
        ]
    return {
        "name": db_game["name"],
        "version": "GOG",
        "slug": details["slug"],
        "game_slug": slugify(db_game["name"]),
        "runner": runner,
        "gogid": db_game["appid"],
        "script": {
            "game": game_config,
            "system": system_config,
            "files": [
                {"goginstaller": "N/A:Select the installer from GOG"}
            ],
            "installer": script
        }
    }
get_dlc_installers(self, db_game)
Source code in lutris/services/gog.py
def get_dlc_installers(self, db_game):
    appid = db_game["service_id"]
    dlcs = self.get_game_dlcs(appid)
    installers = []
    for dlc in dlcs:
        dlc_id = "gogdlc-%s" % dlc["slug"]
        installer = {
            "name": db_game["name"],
            "version": dlc["title"],
            "slug": dlc["slug"],
            "description": "DLC for %s" % db_game["name"],
            "game_slug": slugify(db_game["name"]),
            "runner": "wine",
            "is_dlc": True,
            "dlcid": dlc["id"],
            "gogid": dlc["id"],
            "script": {
                "extends": db_game["installer_slug"],
                "files": [
                    {dlc_id: "N/A:Select the patch from GOG"}
                ],
                "installer": [
                    {"task": {"name": "wineexec", "executable": dlc_id}}
                ]
            }
        }
        installers.append(installer)
    return installers
get_download_info(self, downlink)

Return file download information

Source code in lutris/services/gog.py
def get_download_info(self, downlink):
    """Return file download information"""
    logger.info("Getting download info for %s", downlink)
    try:
        response = self.make_api_request(downlink)
    except HTTPError as ex:
        logger.error("HTTP error: %s", ex)
        raise UnavailableGame from ex
    if not response:
        raise UnavailableGame
    for field in ("checksum", "downlink"):
        field_url = response[field]
        parsed = urlparse(field_url)
        query = dict(parse_qsl(parsed.query))
        response[field + "_filename"] = os.path.basename(query.get("path") or parsed.path)
    return response
get_downloads(self, gogid)

Return all available downloads for a GOG ID

Source code in lutris/services/gog.py
def get_downloads(self, gogid):
    """Return all available downloads for a GOG ID"""
    if not gogid:
        logger.warning("Unable to get GOG data because no GOG ID is available")
        return {}
    gog_data = self.get_game_details(gogid)
    if not gog_data:
        logger.warning("Unable to get GOG data for game %s", gogid)
        return {}
    return gog_data["downloads"]
get_extra_files(self, downloads, installer)
Source code in lutris/services/gog.py
def get_extra_files(self, downloads, installer):
    extra_files = []
    for extra in downloads["bonus_content"]:
        if str(extra["id"]) not in self.selected_extras:
            continue
        links = self.query_download_links(extra)
        for link in links:
            if link["filename"].endswith(".xml"):
                # GOG gives a link for checksum XML files for bonus content
                # but downloading them results in a 404 error.
                continue
            extra_files.append(
                InstallerFile(installer.game_slug, str(extra["id"]), {
                    "url": link["url"],
                    "filename": link["filename"],
                })
            )
    return extra_files
get_extras(self, gogid)

Return a list of bonus content available for a GOG ID and its DLCs

Source code in lutris/services/gog.py
def get_extras(self, gogid):
    """Return a list of bonus content available for a GOG ID and its DLCs"""
    logger.debug("Download extras for GOG ID %s and its DLCs", gogid)
    game = self.get_game_details(gogid)
    if not game:
        logger.warning("Unable to get GOG data for game %s", gogid)
        return []
    dlcs = self.get_game_dlcs(gogid)
    products = [game, *dlcs] if dlcs else [game]
    all_extras = {}
    for product in products:
        extras = [
            {
                "name": download.get("name", "").strip().capitalize(),
                "type": download.get("type", "").strip(),
                "total_size": download.get("total_size", 0),
                "id": str(download["id"]),
            } for download in product["downloads"].get("bonus_content") or []
        ]
        if extras:
            all_extras[product.get("title", "").strip()] = extras
    return all_extras
get_game_details(self, product_id)

Return game information for a given game

Source code in lutris/services/gog.py
def get_game_details(self, product_id):
    """Return game information for a given game"""
    if not product_id:
        raise ValueError("Missing product ID")
    logger.info("Getting game details for %s", product_id)
    url = "{}/products/{}?expand=downloads&locale={}".format(self.api_url, product_id, self.locale)
    return self.make_api_request(url)
get_game_dlcs(self, product_id)

Return the list of DLC products the user owns for a game

Source code in lutris/services/gog.py
def get_game_dlcs(self, product_id):
    """Return the list of DLC products the user owns for a game"""
    game_details = self.get_game_details(product_id)
    if not game_details["dlcs"]:
        return []
    all_products_url = game_details["dlcs"]["expanded_all_products_url"]
    return self.make_api_request(all_products_url)
get_installer_files(self, installer, installer_file_id)
Source code in lutris/services/gog.py
def get_installer_files(self, installer, installer_file_id):
    try:
        downloads = self.get_downloads(installer.service_appid)
    except HTTPError as err:
        raise UnavailableGame("Couldn't load the downloads for this game") from err
    links = self._get_installer_links(installer, downloads)
    if links:
        files = self._format_links(installer, installer_file_id, links)
    else:
        files = []
    if self.selected_extras:
        for extra_file in self.get_extra_files(downloads, installer):
            files.append(extra_file)
    return files
get_installers(self, downloads, runner, language='en')

Return available installers for a GOG game

Source code in lutris/services/gog.py
def get_installers(self, downloads, runner, language="en"):
    """Return available installers for a GOG game"""
    # Filter out Mac installers
    gog_installers = [installer for installer in downloads.get("installers", []) if installer["os"] != "mac"]
    available_platforms = {installer["os"] for installer in gog_installers}
    # If it's a Linux game, also filter out Windows games
    if "linux" in available_platforms:
        filter_os = "windows" if runner == "linux" else "linux"
        gog_installers = [installer for installer in gog_installers if installer["os"] != filter_os]
    return [
        installer
        for installer in gog_installers
        if installer["language"] == self.determine_language_installer(gog_installers, language)
    ]
get_library(self)

Return the user's library of GOG games

Source code in lutris/services/gog.py
def get_library(self):
    """Return the user's library of GOG games"""
    if system.path_exists(self.cache_path):
        logger.debug("Returning cached GOG library")
        with open(self.cache_path, "r", encoding='utf-8') as gog_cache:
            return json.load(gog_cache)

    total_pages = 1
    games = []
    page = 1
    while page <= total_pages:
        products_response = self.get_products_page(page=page)
        page += 1
        total_pages = products_response["totalPages"]
        games += products_response["products"]
    with open(self.cache_path, "w", encoding='utf-8') as gog_cache:
        json.dump(games, gog_cache)
    return games
get_patch_files(self, installer, installer_file_id)
Source code in lutris/services/gog.py
def get_patch_files(self, installer, installer_file_id):
    logger.debug("Getting patches for %s", installer.version)
    downloads = self.get_downloads(installer.service_appid)
    links = []
    for patch_file in downloads["patches"]:
        if "GOG " + patch_file["version"] == installer.version:
            links += self.query_download_links(patch_file)
    return self._format_links(installer, installer_file_id, links)
get_products_page(self, page=1, search=None)

Return a single page of games

Source code in lutris/services/gog.py
def get_products_page(self, page=1, search=None):
    """Return a single page of games"""
    if not self.is_authenticated():
        raise AuthenticationError("User is not logged in")
    params = {"mediaType": "1"}
    if page:
        params["page"] = page
    if search:
        params["search"] = search
    url = self.embed_url + "/account/getFilteredProducts?" + urlencode(params)
    return self.make_request(url)
get_service_game(self, gog_game)
Source code in lutris/services/gog.py
def get_service_game(self, gog_game):
    return GOGGame.new_from_gog_game(gog_game)
get_token_age(self)

Return age of token

Source code in lutris/services/gog.py
def get_token_age(self):
    """Return age of token"""
    token_stat = os.stat(self.token_path)
    token_modified = token_stat.st_mtime
    return time.time() - token_modified
get_update_installers(self, db_game)
Source code in lutris/services/gog.py
def get_update_installers(self, db_game):
    appid = db_game["service_id"]
    patch_versions = self.get_update_versions(appid)
    patch_installers = []
    for version in patch_versions:
        patch = patch_versions[version]
        size = human_size(sum([part["total_size"] for part in patch]))
        patch_id = "gogpatch-%s" % slugify(patch[0]["version"])
        installer = {
            "name": db_game["name"],
            "description": patch[0]["name"] + " " + size,
            "slug": db_game["installer_slug"],
            "game_slug": db_game["slug"],
            "version": "GOG " + patch[0]["version"],
            "runner": "wine",
            "script": {
                "extends": db_game["installer_slug"],
                "files": [
                    {patch_id: "N/A:Select the patch from GOG"}
                ],
                "installer": [
                    {"task": {"name": "wineexec", "executable": patch_id}}
                ]
            }
        }
        patch_installers.append(installer)
    return patch_installers
get_update_versions(self, gog_id)

Return updates available for a game, keyed by patch version

Source code in lutris/services/gog.py
def get_update_versions(self, gog_id):
    """Return updates available for a game, keyed by patch version"""
    games_detail = self.get_game_details(gog_id)
    patches = games_detail["downloads"]["patches"]
    if not patches:
        logger.info("No patches for %s", games_detail)
        return {}
    patch_versions = defaultdict(list)
    for patch in patches:
        patch_versions[patch["name"]].append(patch)
    return patch_versions
get_user_data(self)

Return GOG profile information

Source code in lutris/services/gog.py
def get_user_data(self):
    """Return GOG profile information"""
    url = "https://embed.gog.com/userData.json"
    return self.make_api_request(url)
is_connected(self)

Return whether the user is authenticated and if the service is available

Source code in lutris/services/gog.py
def is_connected(self):
    """Return whether the user is authenticated and if the service is available"""
    if not self.is_authenticated():
        return False
    try:
        user_data = self.get_user_data()
    except UnauthorizedAccess:
        logger.warning("GOG token is invalid")
        return False
    return user_data and "username" in user_data
load(self)

Load the user game library from the GOG API

Source code in lutris/services/gog.py
def load(self):
    """Load the user game library from the GOG API"""
    if self.is_loading:
        logger.warning("GOG games are already loading")
        return
    if not self.is_connected():
        logger.error("User not connected to GOG")
        return
    self.is_loading = True
    games = [GOGGame.new_from_gog_game(game) for game in self.get_library()]
    for game in games:
        game.save()
    self.match_games()
    self.is_loading = False
    return games
load_token(self)

Load token from disk

Source code in lutris/services/gog.py
def load_token(self):
    """Load token from disk"""
    if not os.path.exists(self.token_path):
        raise AuthenticationError("No GOG token available")
    with open(self.token_path, encoding='utf-8') as token_file:
        token_content = json.loads(token_file.read())
    return token_content
login_callback(self, url)
Source code in lutris/services/gog.py
def login_callback(self, url):
    return self.request_token(url)
make_api_request(self, url)

Send a token authenticated request to GOG

Source code in lutris/services/gog.py
def make_api_request(self, url):
    """Send a token authenticated request to GOG"""
    try:
        token = self.load_token()
    except AuthenticationError:
        return
    if self.get_token_age() > 2600:
        self.request_token(refresh_token=token["refresh_token"])
        token = self.load_token()
        if not token:
            logger.warning(
                "Request to %s cancelled because the GOG token could not be acquired",
                url,
            )
            return
    headers = {"Authorization": "Bearer " + token["access_token"]}
    request = Request(url, headers=headers, cookies=self.load_cookies())
    try:
        request.get()
    except HTTPError:
        logger.error(
            "Failed to request %s, check your GOG credentials and internet connectivity",
            url,
        )
        return
    return request.json
make_request(self, url)

Send a cookie authenticated HTTP request to GOG

Source code in lutris/services/gog.py
def make_request(self, url):
    """Send a cookie authenticated HTTP request to GOG"""
    request = Request(url, cookies=self.load_cookies())
    request.get()
    if request.content.startswith(b"<"):
        raise AuthenticationError("Token expired, please log in again")
    return request.json

Convert files from the GOG API to a format compatible with lutris installers

Source code in lutris/services/gog.py
def query_download_links(self, download):
    """Convert files from the GOG API to a format compatible with lutris installers"""
    download_links = []
    for game_file in download.get("files", []):
        downlink = game_file.get("downlink")
        if not downlink:
            logger.error("No download information for %s", game_file)
            continue
        download_info = self.get_download_info(downlink)
        for field in ('checksum', 'downlink'):
            download_links.append({
                "name": download.get("name", ""),
                "os": download.get("os", ""),
                "type": download.get("type", ""),
                "total_size": download.get("total_size", 0),
                "id": str(game_file["id"]),
                "url": download_info[field],
                "filename": download_info[field + "_filename"]
            })
    return download_links
read_file_checksum(self, file_path)

Return the MD5 checksum for a GOG file Requires a GOG XML file as input This has yet to be used.

Source code in lutris/services/gog.py
def read_file_checksum(self, file_path):
    """Return the MD5 checksum for a GOG file
    Requires a GOG XML file as input
    This has yet to be used.
    """
    if not file_path.endswith(".xml"):
        raise ValueError("Pass a XML file to return the checksum")
    with open(file_path, encoding='utf-8') as checksum_file:
        checksum_content = checksum_file.read()
    root_elem = etree.fromstring(checksum_content)
    return (root_elem.attrib["name"], root_elem.attrib["md5"])
request_token(self, url='', refresh_token='')

Get authentication token from GOG

Source code in lutris/services/gog.py
def request_token(self, url="", refresh_token=""):
    """Get authentication token from GOG"""
    if refresh_token:
        grant_type = "refresh_token"
        extra_params = {"refresh_token": refresh_token}
    else:
        grant_type = "authorization_code"
        parsed_url = urlparse(url)
        response_params = dict(parse_qsl(parsed_url.query))
        if "code" not in response_params:
            logger.error("code not received from GOG")
            logger.error(response_params)
            return
        extra_params = {
            "code": response_params["code"],
            "redirect_uri": self.redirect_uri,
        }

    params = {
        "client_id": self.client_id,
        "client_secret": self.client_secret,
        "grant_type": grant_type,
    }
    params.update(extra_params)
    url = "https://auth.gog.com/token?" + urlencode(params)
    request = Request(url)
    try:
        request.get()
    except HTTPError:
        logger.error("Failed to get token, check your GOG credentials.")
        logger.warning("Clearing existing credentials")
        self.logout()
        return

    token = request.json
    with open(self.token_path, "w", encoding='utf-8') as token_file:
        token_file.write(json.dumps(token))
    if not refresh_token:
        self.emit("service-login")

GogLargeBanner (GogSmallBanner)

Big size game logo

Source code in lutris/services/gog.py
class GogLargeBanner(GogSmallBanner):
    """Big size game logo"""
    size = (392, 220)
    dest_path = os.path.join(settings.CACHE_DIR, "gog/banners/large")
    url_pattern = "https:%s_392.jpg"
dest_path
size
url_pattern

GogMediumBanner (GogSmallBanner)

Medium size game logo

Source code in lutris/services/gog.py
class GogMediumBanner(GogSmallBanner):
    """Medium size game logo"""
    size = (196, 110)
    dest_path = os.path.join(settings.CACHE_DIR, "gog/banners/medium")
    url_pattern = "https:%s_196.jpg"
dest_path
size
url_pattern

GogSmallBanner (ServiceMedia)

Small size game logo

Source code in lutris/services/gog.py
class GogSmallBanner(ServiceMedia):
    """Small size game logo"""
    service = "gog"
    size = (100, 60)
    dest_path = os.path.join(settings.CACHE_DIR, "gog/banners/small")
    file_pattern = "%s.jpg"
    api_field = "image"
    url_pattern = "https:%s_prof_game_100x60.jpg"
api_field
dest_path
file_pattern
service
size
url_pattern

humblebundle

Manage Humble Bundle libraries

HumbleBigIcon (HumbleBundleIcon)

Source code in lutris/services/humblebundle.py
class HumbleBigIcon(HumbleBundleIcon):
    size = (105, 105)
size

HumbleBundleGame (ServiceGame)

Service game for DRM free Humble Bundle games

Source code in lutris/services/humblebundle.py
class HumbleBundleGame(ServiceGame):
    """Service game for DRM free Humble Bundle games"""
    service = "humblebundle"

    @classmethod
    def new_from_humble_game(cls, humble_game):
        """Converts a game from the API to a service game usable by Lutris"""
        service_game = HumbleBundleGame()
        service_game.appid = humble_game["machine_name"]
        service_game.slug = humble_game["machine_name"]
        service_game.name = humble_game["human_name"]
        service_game.details = json.dumps(humble_game)
        return service_game
service
new_from_humble_game(humble_game) classmethod

Converts a game from the API to a service game usable by Lutris

Source code in lutris/services/humblebundle.py
@classmethod
def new_from_humble_game(cls, humble_game):
    """Converts a game from the API to a service game usable by Lutris"""
    service_game = HumbleBundleGame()
    service_game.appid = humble_game["machine_name"]
    service_game.slug = humble_game["machine_name"]
    service_game.name = humble_game["human_name"]
    service_game.details = json.dumps(humble_game)
    return service_game

HumbleBundleIcon (ServiceMedia)

HumbleBundle icon

Source code in lutris/services/humblebundle.py
class HumbleBundleIcon(ServiceMedia):
    """HumbleBundle icon"""
    service = "humblebundle"
    size = (70, 70)
    dest_path = os.path.join(settings.CACHE_DIR, "humblebundle/icons")
    file_pattern = "%s.png"
    api_field = "icon"
api_field
dest_path
file_pattern
service
size

HumbleBundleService (OnlineService)

Service for Humble Bundle

Source code in lutris/services/humblebundle.py
class HumbleBundleService(OnlineService):
    """Service for Humble Bundle"""

    id = "humblebundle"
    _matcher = "humble"
    name = _("Humble Bundle")
    icon = "humblebundle"
    online = True
    drm_free = True
    medias = {
        "small_icon": HumbleSmallIcon,
        "icon": HumbleBundleIcon,
        "big_icon": HumbleBigIcon
    }
    default_format = "icon"

    api_url = "https://www.humblebundle.com/"
    login_url = "https://www.humblebundle.com/login?goto=/home/library"
    redirect_uri = "https://www.humblebundle.com/home/library"

    cookies_path = os.path.join(settings.CACHE_DIR, ".humblebundle.auth")
    token_path = os.path.join(settings.CACHE_DIR, ".humblebundle.token")
    cache_path = os.path.join(settings.CACHE_DIR, "humblebundle/library/")

    supported_platforms = ("linux", "windows")
    is_loading = False

    def login_callback(self, url):
        """Called after the user has logged in successfully"""
        self.emit("service-login")

    def is_connected(self):
        """This doesn't actually check if the authentication
        is valid like the GOG service does.
        """
        return self.is_authenticated()

    def load(self):
        """Load the user's Humble Bundle library"""
        if self.is_loading:
            logger.warning("Humble bundle games are already loading")
            return

        self.is_loading = True
        try:
            library = self.get_library()
        except ValueError:
            logger.error("Failed to get Humble Bundle library. Try logging out and back-in.")
            return
        humble_games = []
        seen = set()
        for game in library:
            if game["human_name"] in seen:
                continue
            humble_games.append(HumbleBundleGame.new_from_humble_game(game))
            seen.add(game["human_name"])
        for game in humble_games:
            game.save()
        self.is_loading = False
        return humble_games

    def make_api_request(self, url):
        """Make an authenticated request to the Humble API"""
        request = Request(url, cookies=self.load_cookies())
        try:
            request.get()
        except HTTPError:
            logger.error(
                "Failed to request %s, check your Humble Bundle credentials and internet connectivity",
                url,
            )
            return
        return request.json

    def order_path(self, gamekey):
        """Return the local path for an order"""
        return os.path.join(self.cache_path, "%s.json" % gamekey)

    def get_order(self, gamekey):
        """Retrieve an order identitied by its key"""
        # logger.debug("Getting Humble Bundle order %s", gamekey)
        cache_filename = self.order_path(gamekey)
        if os.path.exists(cache_filename):
            with open(cache_filename, encoding='utf-8') as cache_file:
                return json.load(cache_file)
        response = self.make_api_request(self.api_url + "api/v1/order/%s?all_tpkds=true" % gamekey)
        os.makedirs(self.cache_path, exist_ok=True)
        with open(cache_filename, "w", encoding='utf-8') as cache_file:
            json.dump(response, cache_file)
        return response

    def get_library(self):
        """Return the games from the user's library"""
        games = []
        for order in self.get_orders():
            if not order:
                continue
            for product in order["subproducts"]:
                for download in product["downloads"]:
                    if download["platform"] in self.supported_platforms:
                        games.append(product)
        return games

    def get_gamekeys_from_local_orders(self):
        """Retrieve a list of orders from the cache."""
        game_keys = []
        if os.path.exists(self.cache_path):
            for order_file in os.listdir(self.cache_path):
                if not order_file.endswith(".json"):
                    continue
                game_keys.append({"gamekey": order_file[:-5]})
        return game_keys

    def get_orders(self):
        """Return all orders"""
        gamekeys = self.get_gamekeys_from_local_orders()
        orders = []
        if not gamekeys:
            gamekeys = self.make_api_request(self.api_url + "api/v1/user/order")
        with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
            future_orders = [
                executor.submit(self.get_order, gamekey["gamekey"])
                for gamekey in gamekeys
            ]
            for order in future_orders:
                orders.append(order.result())
        logger.info("Loaded %s Humble Bundle orders", len(orders))
        return orders

    @staticmethod
    def find_download_in_order(order, humbleid, platform):
        """Return the download information in an order for a give game"""
        for product in order["subproducts"]:
            if product["machine_name"] != humbleid:
                continue
            available_platforms = [d["platform"] for d in product["downloads"]]
            if platform not in available_platforms:
                logger.warning("Requested platform %s not available in available platforms: %s",
                               platform, available_platforms)

                if "linux" in available_platforms:
                    platform = "linux"
                elif "windows" in available_platforms:
                    platform = "windows"
                else:
                    platform = available_platforms[0]
            for download in product["downloads"]:
                if download["platform"] != platform:
                    continue
                return {
                    "product": order["product"],
                    "gamekey": order["gamekey"],
                    "created": order["created"],
                    "download": download
                }

    def get_downloads(self, humbleid, platform):
        """Return the download information for a given game"""
        download_links = []
        for order in self.get_orders():
            download = self.find_download_in_order(order, humbleid, platform)
            if download:
                download_links.append(download)
        return download_links

    def get_installer_files(self, installer, installer_file_id):
        """Replace the user provided file with download links from Humble Bundle"""
        try:
            link = get_humble_download_link(installer.service_appid, installer.runner)
        except Exception as ex:
            logger.exception("Failed to get Humble Bundle game: %s", ex)
            raise UnavailableGame from ex
        if not link:
            raise UnavailableGame("No game found on Humble Bundle")
        filename = link.split("?")[0].split("/")[-1]
        return [
            InstallerFile(installer.game_slug, installer_file_id, {
                "url": link,
                "filename": filename
            })
        ]

    @staticmethod
    def get_filename_for_platform(downloads, platform):
        download = [d for d in downloads if d["platform"] == platform][0]
        url = pick_download_url_from_download_info(download)
        if not url:
            return
        return url.split("?")[0].split("/")[-1]

    @staticmethod
    def platform_has_downloads(downloads, platform):
        for download in downloads:
            if download["platform"] != platform:
                continue
            if len(download["download_struct"]) > 0:
                return True

    def generate_installer(self, db_game):
        details = json.loads(db_game["details"])
        platforms = [download["platform"] for download in details["downloads"]]
        system_config = {}
        if "linux" in platforms and self.platform_has_downloads(details["downloads"], "linux"):
            runner = "linux"
            game_config = {"exe": AUTO_ELF_EXE}
            filename = self.get_filename_for_platform(details["downloads"], "linux")
            if filename.lower().endswith(".sh"):
                script = [
                    {"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
                    {"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR", "optional": True}},
                    {"move": {"src": "$CACHE/data/noarch", "dst": "$CACHE/noarch", "optional": True}},
                    {"merge": {"src": "$CACHE/data/x86_64", "dst": "$GAMEDIR", "optional": True}},
                    {"move": {"src": "$CACHE/data/x86_64", "dst": "$CACHE/x86_64", "optional": True}},
                    {"merge": {"src": "$CACHE/data/x86", "dst": "$GAMEDIR", "optional": True}},
                    {"move": {"src": "$CACHE/data/x86", "dst": "$CACHE/x86", "optional": True}},
                    {"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR", "optional": True}},
                ]
            elif filename.endswith("-bin") or filename.endswith("mojo.run"):
                script = [
                    {"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
                    {"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR"}},
                ]
            elif filename.endswith(".air"):
                script = [
                    {"move": {"src": "humblegame", "dst": "$GAMEDIR"}},
                ]
            else:
                script = [{"extract": {"file": "humblegame"}}]
                system_config = {"gamemode": 'false'}  # Unity games crash with gamemode
        elif "windows" in platforms:
            runner = "wine"
            game_config = {"exe": AUTO_WIN32_EXE, "prefix": "$GAMEDIR"}
            filename = self.get_filename_for_platform(details["downloads"], "windows")
            if filename.lower().endswith(".zip"):
                script = [
                    {"task": {"name": "create_prefix", "prefix": "$GAMEDIR"}},
                    {"extract": {"file": "humblegame", "dst": "$GAMEDIR/drive_c/%s" % db_game["slug"]}}
                ]
            else:
                script = [
                    {"task": {"name": "wineexec", "executable": "humblegame"}}
                ]
        else:
            logger.warning("Unsupported platforms: %s", platforms)
            return {}
        return {
            "name": db_game["name"],
            "version": "Humble Bundle",
            "slug": details["machine_name"],
            "game_slug": slugify(db_game["name"]),
            "runner": runner,
            "humbleid": db_game["appid"],
            "script": {
                "game": game_config,
                "system": system_config,
                "files": [
                    {"humblegame": "N/A:Select the installer from Humble Bundle"}
                ],
                "installer": script
            }
        }
api_url
cache_path
cookies_path
default_format
drm_free
icon
id
is_loading
login_url
medias
name
online
redirect_uri
supported_platforms
token_path
find_download_in_order(order, humbleid, platform) staticmethod

Return the download information in an order for a give game

Source code in lutris/services/humblebundle.py
@staticmethod
def find_download_in_order(order, humbleid, platform):
    """Return the download information in an order for a give game"""
    for product in order["subproducts"]:
        if product["machine_name"] != humbleid:
            continue
        available_platforms = [d["platform"] for d in product["downloads"]]
        if platform not in available_platforms:
            logger.warning("Requested platform %s not available in available platforms: %s",
                           platform, available_platforms)

            if "linux" in available_platforms:
                platform = "linux"
            elif "windows" in available_platforms:
                platform = "windows"
            else:
                platform = available_platforms[0]
        for download in product["downloads"]:
            if download["platform"] != platform:
                continue
            return {
                "product": order["product"],
                "gamekey": order["gamekey"],
                "created": order["created"],
                "download": download
            }
generate_installer(self, db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/humblebundle.py
def generate_installer(self, db_game):
    details = json.loads(db_game["details"])
    platforms = [download["platform"] for download in details["downloads"]]
    system_config = {}
    if "linux" in platforms and self.platform_has_downloads(details["downloads"], "linux"):
        runner = "linux"
        game_config = {"exe": AUTO_ELF_EXE}
        filename = self.get_filename_for_platform(details["downloads"], "linux")
        if filename.lower().endswith(".sh"):
            script = [
                {"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
                {"merge": {"src": "$CACHE/data/noarch", "dst": "$GAMEDIR", "optional": True}},
                {"move": {"src": "$CACHE/data/noarch", "dst": "$CACHE/noarch", "optional": True}},
                {"merge": {"src": "$CACHE/data/x86_64", "dst": "$GAMEDIR", "optional": True}},
                {"move": {"src": "$CACHE/data/x86_64", "dst": "$CACHE/x86_64", "optional": True}},
                {"merge": {"src": "$CACHE/data/x86", "dst": "$GAMEDIR", "optional": True}},
                {"move": {"src": "$CACHE/data/x86", "dst": "$CACHE/x86", "optional": True}},
                {"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR", "optional": True}},
            ]
        elif filename.endswith("-bin") or filename.endswith("mojo.run"):
            script = [
                {"extract": {"file": "humblegame", "format": "zip", "dst": "$CACHE"}},
                {"merge": {"src": "$CACHE/data/", "dst": "$GAMEDIR"}},
            ]
        elif filename.endswith(".air"):
            script = [
                {"move": {"src": "humblegame", "dst": "$GAMEDIR"}},
            ]
        else:
            script = [{"extract": {"file": "humblegame"}}]
            system_config = {"gamemode": 'false'}  # Unity games crash with gamemode
    elif "windows" in platforms:
        runner = "wine"
        game_config = {"exe": AUTO_WIN32_EXE, "prefix": "$GAMEDIR"}
        filename = self.get_filename_for_platform(details["downloads"], "windows")
        if filename.lower().endswith(".zip"):
            script = [
                {"task": {"name": "create_prefix", "prefix": "$GAMEDIR"}},
                {"extract": {"file": "humblegame", "dst": "$GAMEDIR/drive_c/%s" % db_game["slug"]}}
            ]
        else:
            script = [
                {"task": {"name": "wineexec", "executable": "humblegame"}}
            ]
    else:
        logger.warning("Unsupported platforms: %s", platforms)
        return {}
    return {
        "name": db_game["name"],
        "version": "Humble Bundle",
        "slug": details["machine_name"],
        "game_slug": slugify(db_game["name"]),
        "runner": runner,
        "humbleid": db_game["appid"],
        "script": {
            "game": game_config,
            "system": system_config,
            "files": [
                {"humblegame": "N/A:Select the installer from Humble Bundle"}
            ],
            "installer": script
        }
    }
get_downloads(self, humbleid, platform)

Return the download information for a given game

Source code in lutris/services/humblebundle.py
def get_downloads(self, humbleid, platform):
    """Return the download information for a given game"""
    download_links = []
    for order in self.get_orders():
        download = self.find_download_in_order(order, humbleid, platform)
        if download:
            download_links.append(download)
    return download_links
get_filename_for_platform(downloads, platform) staticmethod
Source code in lutris/services/humblebundle.py
@staticmethod
def get_filename_for_platform(downloads, platform):
    download = [d for d in downloads if d["platform"] == platform][0]
    url = pick_download_url_from_download_info(download)
    if not url:
        return
    return url.split("?")[0].split("/")[-1]
get_gamekeys_from_local_orders(self)

Retrieve a list of orders from the cache.

Source code in lutris/services/humblebundle.py
def get_gamekeys_from_local_orders(self):
    """Retrieve a list of orders from the cache."""
    game_keys = []
    if os.path.exists(self.cache_path):
        for order_file in os.listdir(self.cache_path):
            if not order_file.endswith(".json"):
                continue
            game_keys.append({"gamekey": order_file[:-5]})
    return game_keys
get_installer_files(self, installer, installer_file_id)

Replace the user provided file with download links from Humble Bundle

Source code in lutris/services/humblebundle.py
def get_installer_files(self, installer, installer_file_id):
    """Replace the user provided file with download links from Humble Bundle"""
    try:
        link = get_humble_download_link(installer.service_appid, installer.runner)
    except Exception as ex:
        logger.exception("Failed to get Humble Bundle game: %s", ex)
        raise UnavailableGame from ex
    if not link:
        raise UnavailableGame("No game found on Humble Bundle")
    filename = link.split("?")[0].split("/")[-1]
    return [
        InstallerFile(installer.game_slug, installer_file_id, {
            "url": link,
            "filename": filename
        })
    ]
get_library(self)

Return the games from the user's library

Source code in lutris/services/humblebundle.py
def get_library(self):
    """Return the games from the user's library"""
    games = []
    for order in self.get_orders():
        if not order:
            continue
        for product in order["subproducts"]:
            for download in product["downloads"]:
                if download["platform"] in self.supported_platforms:
                    games.append(product)
    return games
get_order(self, gamekey)

Retrieve an order identitied by its key

Source code in lutris/services/humblebundle.py
def get_order(self, gamekey):
    """Retrieve an order identitied by its key"""
    # logger.debug("Getting Humble Bundle order %s", gamekey)
    cache_filename = self.order_path(gamekey)
    if os.path.exists(cache_filename):
        with open(cache_filename, encoding='utf-8') as cache_file:
            return json.load(cache_file)
    response = self.make_api_request(self.api_url + "api/v1/order/%s?all_tpkds=true" % gamekey)
    os.makedirs(self.cache_path, exist_ok=True)
    with open(cache_filename, "w", encoding='utf-8') as cache_file:
        json.dump(response, cache_file)
    return response
get_orders(self)

Return all orders

Source code in lutris/services/humblebundle.py
def get_orders(self):
    """Return all orders"""
    gamekeys = self.get_gamekeys_from_local_orders()
    orders = []
    if not gamekeys:
        gamekeys = self.make_api_request(self.api_url + "api/v1/user/order")
    with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor:
        future_orders = [
            executor.submit(self.get_order, gamekey["gamekey"])
            for gamekey in gamekeys
        ]
        for order in future_orders:
            orders.append(order.result())
    logger.info("Loaded %s Humble Bundle orders", len(orders))
    return orders
is_connected(self)

This doesn't actually check if the authentication is valid like the GOG service does.

Source code in lutris/services/humblebundle.py
def is_connected(self):
    """This doesn't actually check if the authentication
    is valid like the GOG service does.
    """
    return self.is_authenticated()
load(self)

Load the user's Humble Bundle library

Source code in lutris/services/humblebundle.py
def load(self):
    """Load the user's Humble Bundle library"""
    if self.is_loading:
        logger.warning("Humble bundle games are already loading")
        return

    self.is_loading = True
    try:
        library = self.get_library()
    except ValueError:
        logger.error("Failed to get Humble Bundle library. Try logging out and back-in.")
        return
    humble_games = []
    seen = set()
    for game in library:
        if game["human_name"] in seen:
            continue
        humble_games.append(HumbleBundleGame.new_from_humble_game(game))
        seen.add(game["human_name"])
    for game in humble_games:
        game.save()
    self.is_loading = False
    return humble_games
login_callback(self, url)

Called after the user has logged in successfully

Source code in lutris/services/humblebundle.py
def login_callback(self, url):
    """Called after the user has logged in successfully"""
    self.emit("service-login")
make_api_request(self, url)

Make an authenticated request to the Humble API

Source code in lutris/services/humblebundle.py
def make_api_request(self, url):
    """Make an authenticated request to the Humble API"""
    request = Request(url, cookies=self.load_cookies())
    try:
        request.get()
    except HTTPError:
        logger.error(
            "Failed to request %s, check your Humble Bundle credentials and internet connectivity",
            url,
        )
        return
    return request.json
order_path(self, gamekey)

Return the local path for an order

Source code in lutris/services/humblebundle.py
def order_path(self, gamekey):
    """Return the local path for an order"""
    return os.path.join(self.cache_path, "%s.json" % gamekey)
platform_has_downloads(downloads, platform) staticmethod
Source code in lutris/services/humblebundle.py
@staticmethod
def platform_has_downloads(downloads, platform):
    for download in downloads:
        if download["platform"] != platform:
            continue
        if len(download["download_struct"]) > 0:
            return True

HumbleSmallIcon (HumbleBundleIcon)

Source code in lutris/services/humblebundle.py
class HumbleSmallIcon(HumbleBundleIcon):
    size = (35, 35)
size

Return a download link for a given humbleid and runner

Source code in lutris/services/humblebundle.py
def get_humble_download_link(humbleid, runner):
    """Return a download link for a given humbleid and runner"""
    service = HumbleBundleService()
    platform = runner if runner != "wine" else "windows"
    downloads = service.get_downloads(humbleid, platform)
    if not downloads:
        logger.error("Game %s for %s not found in the Humble Bundle library", humbleid, platform)
        return
    logger.info("Found %s download for %s", len(downloads), humbleid)
    download = downloads[0]
    logger.info("Reloading order %s", download["product"]["human_name"])
    os.remove(service.order_path(download["gamekey"]))
    order = service.get_order(download["gamekey"])
    download_info = service.find_download_in_order(order, humbleid, platform)
    if download_info:
        return pick_download_url_from_download_info(download_info["download"])
    logger.warning("Couldn't retrieve any downloads for %s", humbleid)

pick_download_url_from_download_info(download_info)

From a list of downloads in Humble Bundle, pick the most appropriate one for the installer. This needs a way to be explicitely filtered.

Source code in lutris/services/humblebundle.py
def pick_download_url_from_download_info(download_info):
    """From a list of downloads in Humble Bundle, pick the most appropriate one
    for the installer.
    This needs a way to be explicitely filtered.
    """
    if not download_info["download_struct"]:
        logger.warning("No downloads found")
        return

    def humble_sort(download):
        name = download["name"]
        if "rpm" in name:
            return -99  # Not supported as an extractor
        bonus = 1
        if "deb" not in name:
            bonus = 2
        if linux.LINUX_SYSTEM.is_64_bit:
            if "386" in name or "32" in name:
                return -1
        else:
            if "64" in name:
                return -10
        return 1 * bonus

    sorted_downloads = sorted(download_info["download_struct"], key=humble_sort, reverse=True)
    logger.debug("Humble bundle installers:")
    for download in sorted_downloads:
        logger.debug(download)
    return sorted_downloads[0]["url"]["web"]

itchio

Itch.io service. Not ready yet.

ItchIoService (OnlineService)

Service class for Itch.io

Source code in lutris/services/itchio.py
class ItchIoService(OnlineService):
    """Service class for Itch.io"""

    id = "itchio"
    name = _("Itch.io (Not implemented)")
    icon = "itchio"
icon
id
name

lutris

LutrisGame (ServiceGame)

Service game created from the Lutris API

Source code in lutris/services/lutris.py
class LutrisGame(ServiceGame):
    """Service game created from the Lutris API"""
    service = "lutris"

    @classmethod
    def new_from_api(cls, api_payload):
        """Create an instance of LutrisGame from the API response"""
        service_game = LutrisGame()
        service_game.appid = api_payload['slug']
        service_game.slug = api_payload['slug']
        service_game.name = api_payload['name']
        service_game.details = json.dumps(api_payload)
        return service_game
service
new_from_api(api_payload) classmethod

Create an instance of LutrisGame from the API response

Source code in lutris/services/lutris.py
@classmethod
def new_from_api(cls, api_payload):
    """Create an instance of LutrisGame from the API response"""
    service_game = LutrisGame()
    service_game.appid = api_payload['slug']
    service_game.slug = api_payload['slug']
    service_game.name = api_payload['name']
    service_game.details = json.dumps(api_payload)
    return service_game

LutrisService (OnlineService)

Service for Lutris games

Source code in lutris/services/lutris.py
class LutrisService(OnlineService):
    """Service for Lutris games"""

    id = "lutris"
    name = _("Lutris")
    icon = "lutris"
    online = True
    medias = {
        "icon": LutrisIcon,
        "banner": LutrisBanner,
        "coverart_med": LutrisCoverartMedium,
        "coverart_big": LutrisCoverart,
    }
    default_format = "banner"

    api_url = settings.SITE_URL + "/api"
    login_url = settings.SITE_URL + "/api/accounts/token"
    cache_path = os.path.join(settings.CACHE_DIR, "lutris")
    token_path = os.path.join(settings.CACHE_DIR, "auth-token")

    is_loading = False

    @property
    def credential_files(self):
        """Return a list of all files used for authentication"""
        return [self.token_path]

    def match_games(self):
        """Matching lutris games is much simpler... No API call needed."""
        service_games = {
            str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
        }
        for lutris_game in get_games():
            self.match_game(service_games.get(lutris_game["slug"]), lutris_game)

    def is_connected(self):
        """Is the service connected?"""
        return self.is_authenticated()

    def login(self, parent=None):
        """Connect to Lutris"""
        login_dialog = dialogs.ClientLoginDialog(parent=parent)
        login_dialog.connect("connected", self.on_connect_success)

    def on_connect_success(self, _widget, _username):
        """Handles connection success"""
        self.emit("service-login")

    def get_library(self):
        """Return the remote library as a list of dicts."""
        credentials = read_api_key()
        if not credentials:
            return []
        url = settings.SITE_URL + "/api/games/library/%s" % urllib.parse.quote(credentials["username"])
        request = http.Request(url, headers={"Authorization": "Token " + credentials["token"]})
        try:
            response = request.get()
        except http.HTTPError as ex:
            logger.error("Unable to load library: %s", ex)
            return []
        response_data = response.json
        if response_data:
            return response_data["games"]
        return []

    def load(self):
        if self.is_loading:
            logger.warning("Lutris games are already loading")
            return
        self.is_loading = True
        lutris_games = self.get_library()
        logger.debug("Loaded %s games from Lutris library", len(lutris_games))
        for game in lutris_games:
            lutris_game = LutrisGame.new_from_api(game)
            lutris_game.save()
        logger.debug("Matching with already installed games")
        self.match_games()
        self.is_loading = False
        logger.debug("Lutris games loaded")
        return lutris_games

    def install(self, db_game):
        if isinstance(db_game, dict):
            slug = db_game["slug"]
        else:
            slug = db_game
        installers = get_game_installers(slug)
        if not installers:
            logger.warning("No installer for %s", slug)
            return
        application = Gio.Application.get_default()
        application.show_installer_window(installers)
api_url
cache_path
credential_files property readonly

Return a list of all files used for authentication

default_format
icon
id
is_loading
login_url
medias
name
online
token_path
get_library(self)

Return the remote library as a list of dicts.

Source code in lutris/services/lutris.py
def get_library(self):
    """Return the remote library as a list of dicts."""
    credentials = read_api_key()
    if not credentials:
        return []
    url = settings.SITE_URL + "/api/games/library/%s" % urllib.parse.quote(credentials["username"])
    request = http.Request(url, headers={"Authorization": "Token " + credentials["token"]})
    try:
        response = request.get()
    except http.HTTPError as ex:
        logger.error("Unable to load library: %s", ex)
        return []
    response_data = response.json
    if response_data:
        return response_data["games"]
    return []
install(self, db_game)

Install a service game, or starts the installer of the game.

Parameters:

Name Type Description Default
db_game dict or str

Database fields of the game to add, or (for Lutris service only the slug of the game.)

required

Returns:

Type Description
str

The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None.

Source code in lutris/services/lutris.py
def install(self, db_game):
    if isinstance(db_game, dict):
        slug = db_game["slug"]
    else:
        slug = db_game
    installers = get_game_installers(slug)
    if not installers:
        logger.warning("No installer for %s", slug)
        return
    application = Gio.Application.get_default()
    application.show_installer_window(installers)
is_connected(self)

Is the service connected?

Source code in lutris/services/lutris.py
def is_connected(self):
    """Is the service connected?"""
    return self.is_authenticated()
load(self)
Source code in lutris/services/lutris.py
def load(self):
    if self.is_loading:
        logger.warning("Lutris games are already loading")
        return
    self.is_loading = True
    lutris_games = self.get_library()
    logger.debug("Loaded %s games from Lutris library", len(lutris_games))
    for game in lutris_games:
        lutris_game = LutrisGame.new_from_api(game)
        lutris_game.save()
    logger.debug("Matching with already installed games")
    self.match_games()
    self.is_loading = False
    logger.debug("Lutris games loaded")
    return lutris_games
login(self, parent=None)

Connect to Lutris

Source code in lutris/services/lutris.py
def login(self, parent=None):
    """Connect to Lutris"""
    login_dialog = dialogs.ClientLoginDialog(parent=parent)
    login_dialog.connect("connected", self.on_connect_success)
match_games(self)

Matching lutris games is much simpler... No API call needed.

Source code in lutris/services/lutris.py
def match_games(self):
    """Matching lutris games is much simpler... No API call needed."""
    service_games = {
        str(game["appid"]): game for game in ServiceGameCollection.get_for_service(self.id)
    }
    for lutris_game in get_games():
        self.match_game(service_games.get(lutris_game["slug"]), lutris_game)
on_connect_success(self, _widget, _username)

Handles connection success

Source code in lutris/services/lutris.py
def on_connect_success(self, _widget, _username):
    """Handles connection success"""
    self.emit("service-login")

download_lutris_media(slug)

Download all media types for a single lutris game

Source code in lutris/services/lutris.py
def download_lutris_media(slug):
    """Download all media types for a single lutris game"""
    url = settings.SITE_URL + "/api/games/%s" % slug
    request = http.Request(url)
    try:
        response = request.get()
    except http.HTTPError as ex:
        logger.debug("Unable to load %s: %s", slug, ex)
        return
    response_data = response.json
    icon_url = response_data.get("icon_url")
    if icon_url:
        download_media({slug: icon_url}, LutrisIcon())

    banner_url = response_data.get("banner_url")
    if banner_url:
        download_media({slug: banner_url}, LutrisBanner())

    cover_url = response_data.get("coverart")
    if cover_url:
        download_media({slug: cover_url}, LutrisCoverart())

sync_media()

Downlad all missing media

Source code in lutris/services/lutris.py
def sync_media():
    """Downlad all missing media"""
    banners_available = {fn.split(".")[0] for fn in os.listdir(settings.BANNER_PATH)}
    icons_available = {
        fn.split(".")[0].replace("lutris_", "")
        for fn in os.listdir(settings.ICON_PATH)
        if fn.startswith("lutris_")
    }
    covers_available = {fn.split(".")[0] for fn in os.listdir(settings.COVERART_PATH)}
    complete_games = banners_available.intersection(icons_available).intersection(covers_available)
    all_slugs = {game["slug"] for game in get_games()}
    slugs = all_slugs - complete_games
    if not slugs:
        return
    games = get_api_games(list(slugs))

    alias_map = {}
    api_slugs = set()
    for game in games:
        api_slugs.add(game["slug"])
        for alias in game["aliases"]:
            if alias["slug"] in slugs:
                alias_map[game["slug"]] = alias["slug"]
    alias_slugs = set(alias_map.values())
    used_alias_slugs = alias_slugs - api_slugs
    for alias_slug in used_alias_slugs:
        for game in games:
            if alias_slug in [alias["slug"] for alias in game["aliases"]]:
                game["slug"] = alias_map[game["slug"]]
                continue
    banner_urls = {
        game["slug"]: game["banner_url"]
        for game in games
        if game["slug"] not in banners_available and game["banner_url"]
    }
    icon_urls = {
        game["slug"]: game["icon_url"]
        for game in games
        if game["slug"] not in icons_available and game["icon_url"]
    }
    cover_urls = {
        game["slug"]: game["coverart"]
        for game in games
        if game["slug"] not in covers_available and game["coverart"]
    }
    logger.debug(
        "Syncing %s banners, %s icons and %s covers",
        len(banner_urls), len(icon_urls), len(cover_urls)
    )
    download_media(banner_urls, LutrisBanner())
    download_media(icon_urls, LutrisIcon())
    download_media(cover_urls, LutrisCoverart())

mame

MAME service Not ready yet

MAMEService (BaseService)

Service class for MAME

Source code in lutris/services/mame.py
class MAMEService(BaseService):
    """Service class for MAME"""
    id = "mame"
    name = _("MAME")
    icon = "mame"
icon
id
name

origin

EA Origin service.

OriginGame (ServiceGame)

Source code in lutris/services/origin.py
class OriginGame(ServiceGame):
    service = "origin"

    @classmethod
    def new_from_api(cls, offer):
        origin_game = OriginGame()
        origin_game.appid = offer["offerId"]
        origin_game.slug = offer["gameNameFacetKey"]
        origin_game.name = offer["i18n"]["displayName"]
        origin_game.details = json.dumps(offer)
        return origin_game
service
new_from_api(offer) classmethod
Source code in lutris/services/origin.py
@classmethod
def new_from_api(cls, offer):
    origin_game = OriginGame()
    origin_game.appid = offer["offerId"]
    origin_game.slug = offer["gameNameFacetKey"]
    origin_game.name = offer["i18n"]["displayName"]
    origin_game.details = json.dumps(offer)
    return origin_game

OriginLauncher

Source code in lutris/services/origin.py
class OriginLauncher:
    manifests_paths = "ProgramData/Origin/LocalContent"

    def __init__(self, prefix_path):
        self.prefix_path = prefix_path

    def iter_manifests(self):
        manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
        if not os.path.exists(manifests_path):
            logger.warning("No directory in %s", manifests_path)
            return
        for game_folder in os.listdir(manifests_path):
            for manifest in os.listdir(os.path.join(manifests_path, game_folder)):
                if not manifest.endswith(".mfst"):
                    continue
                with open(os.path.join(manifests_path, game_folder, manifest), encoding="utf-8") as manifest_file:
                    manifest_content = manifest_file.read()
                parsed_url = urllib.parse.urlparse(manifest_content)
                parsed_data = dict(urllib.parse.parse_qsl(parsed_url.query))
                yield parsed_data
manifests_paths
__init__(self, prefix_path) special
Source code in lutris/services/origin.py
def __init__(self, prefix_path):
    self.prefix_path = prefix_path
iter_manifests(self)
Source code in lutris/services/origin.py
def iter_manifests(self):
    manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
    if not os.path.exists(manifests_path):
        logger.warning("No directory in %s", manifests_path)
        return
    for game_folder in os.listdir(manifests_path):
        for manifest in os.listdir(os.path.join(manifests_path, game_folder)):
            if not manifest.endswith(".mfst"):
                continue
            with open(os.path.join(manifests_path, game_folder, manifest), encoding="utf-8") as manifest_file:
                manifest_content = manifest_file.read()
            parsed_url = urllib.parse.urlparse(manifest_content)
            parsed_data = dict(urllib.parse.parse_qsl(parsed_url.query))
            yield parsed_data

OriginPackArtLarge (OriginPackArtSmall)

Source code in lutris/services/origin.py
class OriginPackArtLarge(OriginPackArtSmall):
    size = (231, 326)
    dest_path = os.path.join(settings.CACHE_DIR, "origin/pack-art-large")
    api_field = "packArtLarge"
api_field
dest_path
size

OriginPackArtMedium (OriginPackArtSmall)

Source code in lutris/services/origin.py
class OriginPackArtMedium(OriginPackArtSmall):
    size = (142, 200)
    dest_path = os.path.join(settings.CACHE_DIR, "origin/pack-art-medium")
    api_field = "packArtMedium"
api_field
dest_path
size

OriginPackArtSmall (ServiceMedia)

Source code in lutris/services/origin.py
class OriginPackArtSmall(ServiceMedia):
    service = "origin"
    file_pattern = "%s.jpg"
    size = (63, 89)
    dest_path = os.path.join(settings.CACHE_DIR, "origin/pack-art-small")
    api_field = "packArtSmall"

    def get_media_url(self, details):
        return details["imageServer"] + details["i18n"][self.api_field]
api_field
dest_path
file_pattern
service
size
get_media_url(self, details)
Source code in lutris/services/origin.py
def get_media_url(self, details):
    return details["imageServer"] + details["i18n"][self.api_field]

OriginService (OnlineService)

Service class for EA Origin

Source code in lutris/services/origin.py
class OriginService(OnlineService):
    """Service class for EA Origin"""

    id = "origin"
    name = _("Origin")
    icon = "origin"
    client_installer = "origin"
    runner = "wine"
    online = True
    medias = {
        "packArtSmall": OriginPackArtSmall,
        "packArtMedium": OriginPackArtMedium,
        "packArtLarge": OriginPackArtLarge,
    }
    default_format = "packArtMedium"
    cache_path = os.path.join(settings.CACHE_DIR, "origin/cache/")
    cookies_path = os.path.join(settings.CACHE_DIR, "origin/cookies")
    token_path = os.path.join(settings.CACHE_DIR, "origin/auth_token")
    redirect_uri = "https://www.origin.com/views/login.html"
    login_url = (
        "https://accounts.ea.com/connect/auth"
        "?response_type=code&client_id=ORIGIN_SPA_ID&display=originXWeb/login"
        "&locale=en_US&release_type=prod"
        "&redirect_uri=%s"
    ) % redirect_uri
    is_loading = False

    def __init__(self):
        super().__init__()

        self.session = requests.session()
        self.access_token = self.load_access_token()

    @property
    def api_url(self):
        return "https://api%s.origin.com" % random.randint(1, 4)

    def run(self):
        db_game = get_game_by_field(self.client_installer, "slug")
        game = Game(db_game["id"])
        game.emit("game-launch")

    def is_launchable(self):
        return get_game_by_field(self.client_installer, "slug")

    def is_connected(self):
        return bool(self.access_token)

    def login_callback(self, url):
        self.fetch_access_token()
        self.emit("service-login")

    def fetch_access_token(self):
        token_data = self.get_access_token()
        if not token_data:
            raise RuntimeError("Failed to get access token")
        with open(self.token_path, "w", encoding='utf-8') as token_file:
            token_file.write(json.dumps(token_data, indent=2))
        self.access_token = self.load_access_token()

    def load_access_token(self):
        if not os.path.exists(self.token_path):
            return ""
        with open(self.token_path) as token_file:
            token_data = json.load(token_file)
            return token_data.get("access_token", "")

    def get_access_token(self):
        """Request an access token from EA"""
        response = self.session.get(
            "https://accounts.ea.com/connect/auth",
            params={
                "client_id": "ORIGIN_JS_SDK",
                "response_type": "token",
                "redirect_uri": "nucleus:rest",
                "prompt": "none"
            },
            cookies=self.load_cookies()
        )
        response.raise_for_status()
        token_data = response.json()
        return token_data

    def _request_identity(self):
        response = self.session.get(
            "https://gateway.ea.com/proxy/identity/pids/me",
            cookies=self.load_cookies(),
            headers=self.get_auth_headers()
        )
        return response.json()

    def get_identity(self):
        """Request the user info"""
        identity_data = self._request_identity()
        if identity_data.get('error') == "invalid_access_token":
            logger.warning("Refreshing Origin access token")
            self.fetch_access_token()
            identity_data = self._request_identity()
        elif identity_data.get("error"):
            raise RuntimeError(
                "%s (Error code: %s)" % (identity_data["error"], identity_data["error_number"])
            )

        if 'error' in identity_data:
            raise RuntimeError(identity_data["error"])
        try:
            user_id = identity_data["pid"]["pidId"]
        except KeyError:
            logger.error("Can't read user ID from %s", identity_data)
            raise

        persona_id_response = self.session.get(
            "{}/atom/users?userIds={}".format(self.api_url, user_id),
            headers=self.get_auth_headers()
        )
        content = persona_id_response.text
        origin_account_info = ElementTree.fromstring(content)
        persona_id = origin_account_info.find("user").find("personaId").text
        user_name = origin_account_info.find("user").find("EAID").text
        return str(user_id), str(persona_id), str(user_name)

    def load(self):
        if self.is_loading:
            logger.warning("Origin games are already loading")
            return
        user_id, _persona_id, _user_name = self.get_identity()
        self.is_loading = True
        games = self.get_library(user_id)
        logger.info("Retrieved %s games from Origin library", len(games))
        origin_games = []
        for game in games:
            origin_game = OriginGame.new_from_api(game)
            origin_game.save()
            origin_games.append(origin_game)
        self.is_loading = False
        return origin_games

    def get_library(self, user_id):
        """Load Origin library"""
        offers = []
        for entitlement in self.get_entitlements(user_id):
            if entitlement["offerType"] != "basegame":
                continue
            offer_id = entitlement["offerId"]
            offer = self.get_offer(offer_id)
            offers.append(offer)
        return offers

    def get_offer(self, offer_id):
        """Load offer details from Origin"""
        url = "{}/ecommerce2/public/supercat/{}/{}".format(self.api_url, offer_id, "en_US")
        response = self.session.get(url, headers=self.get_auth_headers())
        return response.json()

    def get_entitlements(self, user_id):
        """Request the user's entitlements"""
        url = "%s/ecommerce2/consolidatedentitlements/%s?machine_hash=1" % (
            self.api_url,
            user_id
        )
        headers = self.get_auth_headers()
        headers["Accept"] = "application/vnd.origin.v3+json; x-cache/force-write"
        response = self.session.get(url, headers=headers)
        data = response.json()
        return data["entitlements"]

    def get_auth_headers(self):
        """Return headers needed to authenticate HTTP requests"""
        if not self.access_token:
            raise RuntimeError("User not authenticated to Origin")
        return {
            "Authorization": "Bearer %s" % self.access_token,
            "AuthToken": self.access_token,
            "X-AuthToken": self.access_token
        }

    def add_installed_games(self):
        origin_game = get_game_by_field("origin", "slug")
        if not origin_game:
            logger.error("Origin is not installed")
        origin_prefix = origin_game["directory"].split("drive_c")[0]
        if not os.path.exists(os.path.join(origin_prefix, "drive_c")):
            logger.error("Invalid install of Origin at %s", origin_prefix)
            return
        origin_launcher = OriginLauncher(origin_prefix)
        installed_games = 0
        for manifest in origin_launcher.iter_manifests():
            self.install_from_origin(origin_game, manifest)
            installed_games += 1
        logger.debug("Installed %s Origin games", installed_games)

    def install_from_origin(self, origin_game, manifest):
        offer_id = manifest["id"].split("@")[0]
        logger.debug("Installing Origin game %s", offer_id)
        service_game = ServiceGameCollection.get_game("origin", offer_id)
        if not service_game:
            logger.error("Aborting install, %s is not present in the game library.", offer_id)
            return
        lutris_game_id = slugify(service_game["name"]) + "-" + self.id
        existing_game = get_game_by_field(lutris_game_id, "installer_slug")
        if existing_game:
            return
        game_config = LutrisConfig(game_config_id=origin_game["configpath"]).game_level
        game_config["game"]["args"] = get_launch_arguments(manifest["id"])
        configpath = write_game_config(lutris_game_id, game_config)
        game_id = add_game(
            name=service_game["name"],
            runner=origin_game["runner"],
            slug=slugify(service_game["name"]),
            directory=origin_game["directory"],
            installed=1,
            installer_slug=lutris_game_id,
            configpath=configpath,
            service=self.id,
            service_id=offer_id,
        )
        return game_id

    def generate_installer(self, db_game, origin_db_game):
        origin_game = Game(origin_db_game["id"])
        origin_exe = origin_game.config.game_config["exe"]
        if not os.path.isabs(origin_exe):
            origin_exe = os.path.join(origin_game.config.game_config["prefix"], origin_exe)
        return {
            "name": db_game["name"],
            "version": self.name,
            "slug": slugify(db_game["name"]) + "-" + self.id,
            "game_slug": slugify(db_game["name"]),
            "runner": self.runner,
            "appid": db_game["appid"],
            "script": {
                "requires": self.client_installer,
                "game": {
                    "args": get_launch_arguments(db_game["appid"]),
                },
                "installer": [
                    {"task": {
                        "name": "wineexec",
                        "executable": origin_exe,
                        "args": get_launch_arguments(db_game["appid"], "download"),
                        "prefix": origin_game.config.game_config["prefix"],
                        "description": (
                            "Origin will now open and install %s." % db_game["name"]
                        )
                    }}
                ]
            }
        }

    def install(self, db_game):
        origin_game = get_game_by_field(self.client_installer, "slug")
        application = Gio.Application.get_default()
        if not origin_game or not origin_game["installed"]:
            logger.warning("Installing the Origin client")
            installers = get_installers(game_slug=self.client_installer)
            application.show_installer_window(installers)
        else:
            application.show_installer_window(
                [self.generate_installer(db_game, origin_game)],
                service=self,
                appid=db_game["appid"]
            )
api_url property readonly
cache_path
client_installer
cookies_path
default_format
icon
id
is_loading
login_url
medias
name
online
redirect_uri
runner
token_path
__init__(self) special
Source code in lutris/services/origin.py
def __init__(self):
    super().__init__()

    self.session = requests.session()
    self.access_token = self.load_access_token()
add_installed_games(self)

Services can implement this method to scan for locally installed games and add them to lutris.

Source code in lutris/services/origin.py
def add_installed_games(self):
    origin_game = get_game_by_field("origin", "slug")
    if not origin_game:
        logger.error("Origin is not installed")
    origin_prefix = origin_game["directory"].split("drive_c")[0]
    if not os.path.exists(os.path.join(origin_prefix, "drive_c")):
        logger.error("Invalid install of Origin at %s", origin_prefix)
        return
    origin_launcher = OriginLauncher(origin_prefix)
    installed_games = 0
    for manifest in origin_launcher.iter_manifests():
        self.install_from_origin(origin_game, manifest)
        installed_games += 1
    logger.debug("Installed %s Origin games", installed_games)
fetch_access_token(self)
Source code in lutris/services/origin.py
def fetch_access_token(self):
    token_data = self.get_access_token()
    if not token_data:
        raise RuntimeError("Failed to get access token")
    with open(self.token_path, "w", encoding='utf-8') as token_file:
        token_file.write(json.dumps(token_data, indent=2))
    self.access_token = self.load_access_token()
generate_installer(self, db_game, origin_db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/origin.py
def generate_installer(self, db_game, origin_db_game):
    origin_game = Game(origin_db_game["id"])
    origin_exe = origin_game.config.game_config["exe"]
    if not os.path.isabs(origin_exe):
        origin_exe = os.path.join(origin_game.config.game_config["prefix"], origin_exe)
    return {
        "name": db_game["name"],
        "version": self.name,
        "slug": slugify(db_game["name"]) + "-" + self.id,
        "game_slug": slugify(db_game["name"]),
        "runner": self.runner,
        "appid": db_game["appid"],
        "script": {
            "requires": self.client_installer,
            "game": {
                "args": get_launch_arguments(db_game["appid"]),
            },
            "installer": [
                {"task": {
                    "name": "wineexec",
                    "executable": origin_exe,
                    "args": get_launch_arguments(db_game["appid"], "download"),
                    "prefix": origin_game.config.game_config["prefix"],
                    "description": (
                        "Origin will now open and install %s." % db_game["name"]
                    )
                }}
            ]
        }
    }
get_access_token(self)

Request an access token from EA

Source code in lutris/services/origin.py
def get_access_token(self):
    """Request an access token from EA"""
    response = self.session.get(
        "https://accounts.ea.com/connect/auth",
        params={
            "client_id": "ORIGIN_JS_SDK",
            "response_type": "token",
            "redirect_uri": "nucleus:rest",
            "prompt": "none"
        },
        cookies=self.load_cookies()
    )
    response.raise_for_status()
    token_data = response.json()
    return token_data
get_auth_headers(self)

Return headers needed to authenticate HTTP requests

Source code in lutris/services/origin.py
def get_auth_headers(self):
    """Return headers needed to authenticate HTTP requests"""
    if not self.access_token:
        raise RuntimeError("User not authenticated to Origin")
    return {
        "Authorization": "Bearer %s" % self.access_token,
        "AuthToken": self.access_token,
        "X-AuthToken": self.access_token
    }
get_entitlements(self, user_id)

Request the user's entitlements

Source code in lutris/services/origin.py
def get_entitlements(self, user_id):
    """Request the user's entitlements"""
    url = "%s/ecommerce2/consolidatedentitlements/%s?machine_hash=1" % (
        self.api_url,
        user_id
    )
    headers = self.get_auth_headers()
    headers["Accept"] = "application/vnd.origin.v3+json; x-cache/force-write"
    response = self.session.get(url, headers=headers)
    data = response.json()
    return data["entitlements"]
get_identity(self)

Request the user info

Source code in lutris/services/origin.py
def get_identity(self):
    """Request the user info"""
    identity_data = self._request_identity()
    if identity_data.get('error') == "invalid_access_token":
        logger.warning("Refreshing Origin access token")
        self.fetch_access_token()
        identity_data = self._request_identity()
    elif identity_data.get("error"):
        raise RuntimeError(
            "%s (Error code: %s)" % (identity_data["error"], identity_data["error_number"])
        )

    if 'error' in identity_data:
        raise RuntimeError(identity_data["error"])
    try:
        user_id = identity_data["pid"]["pidId"]
    except KeyError:
        logger.error("Can't read user ID from %s", identity_data)
        raise

    persona_id_response = self.session.get(
        "{}/atom/users?userIds={}".format(self.api_url, user_id),
        headers=self.get_auth_headers()
    )
    content = persona_id_response.text
    origin_account_info = ElementTree.fromstring(content)
    persona_id = origin_account_info.find("user").find("personaId").text
    user_name = origin_account_info.find("user").find("EAID").text
    return str(user_id), str(persona_id), str(user_name)
get_library(self, user_id)

Load Origin library

Source code in lutris/services/origin.py
def get_library(self, user_id):
    """Load Origin library"""
    offers = []
    for entitlement in self.get_entitlements(user_id):
        if entitlement["offerType"] != "basegame":
            continue
        offer_id = entitlement["offerId"]
        offer = self.get_offer(offer_id)
        offers.append(offer)
    return offers
get_offer(self, offer_id)

Load offer details from Origin

Source code in lutris/services/origin.py
def get_offer(self, offer_id):
    """Load offer details from Origin"""
    url = "{}/ecommerce2/public/supercat/{}/{}".format(self.api_url, offer_id, "en_US")
    response = self.session.get(url, headers=self.get_auth_headers())
    return response.json()
install(self, db_game)

Install a service game, or starts the installer of the game.

Parameters:

Name Type Description Default
db_game dict or str

Database fields of the game to add, or (for Lutris service only the slug of the game.)

required

Returns:

Type Description
str

The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None.

Source code in lutris/services/origin.py
def install(self, db_game):
    origin_game = get_game_by_field(self.client_installer, "slug")
    application = Gio.Application.get_default()
    if not origin_game or not origin_game["installed"]:
        logger.warning("Installing the Origin client")
        installers = get_installers(game_slug=self.client_installer)
        application.show_installer_window(installers)
    else:
        application.show_installer_window(
            [self.generate_installer(db_game, origin_game)],
            service=self,
            appid=db_game["appid"]
        )
install_from_origin(self, origin_game, manifest)
Source code in lutris/services/origin.py
def install_from_origin(self, origin_game, manifest):
    offer_id = manifest["id"].split("@")[0]
    logger.debug("Installing Origin game %s", offer_id)
    service_game = ServiceGameCollection.get_game("origin", offer_id)
    if not service_game:
        logger.error("Aborting install, %s is not present in the game library.", offer_id)
        return
    lutris_game_id = slugify(service_game["name"]) + "-" + self.id
    existing_game = get_game_by_field(lutris_game_id, "installer_slug")
    if existing_game:
        return
    game_config = LutrisConfig(game_config_id=origin_game["configpath"]).game_level
    game_config["game"]["args"] = get_launch_arguments(manifest["id"])
    configpath = write_game_config(lutris_game_id, game_config)
    game_id = add_game(
        name=service_game["name"],
        runner=origin_game["runner"],
        slug=slugify(service_game["name"]),
        directory=origin_game["directory"],
        installed=1,
        installer_slug=lutris_game_id,
        configpath=configpath,
        service=self.id,
        service_id=offer_id,
    )
    return game_id
is_connected(self)
Source code in lutris/services/origin.py
def is_connected(self):
    return bool(self.access_token)
is_launchable(self)
Source code in lutris/services/origin.py
def is_launchable(self):
    return get_game_by_field(self.client_installer, "slug")
load(self)
Source code in lutris/services/origin.py
def load(self):
    if self.is_loading:
        logger.warning("Origin games are already loading")
        return
    user_id, _persona_id, _user_name = self.get_identity()
    self.is_loading = True
    games = self.get_library(user_id)
    logger.info("Retrieved %s games from Origin library", len(games))
    origin_games = []
    for game in games:
        origin_game = OriginGame.new_from_api(game)
        origin_game.save()
        origin_games.append(origin_game)
    self.is_loading = False
    return origin_games
load_access_token(self)
Source code in lutris/services/origin.py
def load_access_token(self):
    if not os.path.exists(self.token_path):
        return ""
    with open(self.token_path) as token_file:
        token_data = json.load(token_file)
        return token_data.get("access_token", "")
login_callback(self, url)
Source code in lutris/services/origin.py
def login_callback(self, url):
    self.fetch_access_token()
    self.emit("service-login")
run(self)

Override this method to run a launcher

Source code in lutris/services/origin.py
def run(self):
    db_game = get_game_by_field(self.client_installer, "slug")
    game = Game(db_game["id"])
    game.emit("game-launch")

get_launch_arguments(offer_id, action='launch')

Source code in lutris/services/origin.py
def get_launch_arguments(offer_id, action="launch"):
    if action == "launch":
        return "origin2://game/launch?offerIds=%s&autoDownload=1" % offer_id
    if action == "download":
        return "origin2://game/download?offerId=%s" % offer_id

scummvm

Legacy ScummVM 'service', has to be ported to the current architecture

ICON

NAME

ONLINE

SCUMMVM_CONFIG_FILE

get_scummvm_games()

Return the available ScummVM games

Source code in lutris/services/scummvm.py
def get_scummvm_games():
    """Return the available ScummVM games"""
    if not system.path_exists(SCUMMVM_CONFIG_FILE):
        logger.info("No ScummVM config found")
        return []
    config = ConfigParser()
    config.read(SCUMMVM_CONFIG_FILE)
    config_sections = config.sections()
    for section in config_sections:
        if section == "scummvm":
            continue
        scummvm_id = section
        name = re.split(r" \(.*\)$", config[section]["description"])[0]
        path = config[section]["path"]
        yield (scummvm_id, name, path)

service_game

Service game module

PGA_DB

ServiceGame

Representation of a game from a 3rd party service

Source code in lutris/services/service_game.py
class ServiceGame:
    """Representation of a game from a 3rd party service"""

    service = NotImplemented
    installer_slug = NotImplemented
    medias = (ServiceMedia, )

    def __init__(self):
        self.appid = None  # External ID of the game on the 3rd party service
        self.game_id = None  # Internal Lutris ID
        self.runner = None  # Name of the runner
        self.name = None  # Name
        self.slug = None  # Game slug
        self.lutris_slug = None  # Slug used by the lutris website
        self.logo = None  # Game logo
        self.icon = None  # Game icon
        self.details = None  # Additional details for the game

    def save(self):
        """Save this game to database"""
        game_data = {
            "service": self.service,
            "appid": self.appid,
            "name": self.name,
            "slug": self.slug,
            "lutris_slug": self.lutris_slug,
            "icon": self.icon,
            "logo": self.logo,
            "details": str(self.details),
        }
        existing_game = ServiceGameCollection.get_game(self.service, self.appid)
        if existing_game:
            sql.db_update(PGA_DB, "service_games", game_data, {"id": existing_game["id"]})
        else:
            sql.db_insert(PGA_DB, "service_games", game_data)
installer_slug
medias
service
__init__(self) special
Source code in lutris/services/service_game.py
def __init__(self):
    self.appid = None  # External ID of the game on the 3rd party service
    self.game_id = None  # Internal Lutris ID
    self.runner = None  # Name of the runner
    self.name = None  # Name
    self.slug = None  # Game slug
    self.lutris_slug = None  # Slug used by the lutris website
    self.logo = None  # Game logo
    self.icon = None  # Game icon
    self.details = None  # Additional details for the game
save(self)

Save this game to database

Source code in lutris/services/service_game.py
def save(self):
    """Save this game to database"""
    game_data = {
        "service": self.service,
        "appid": self.appid,
        "name": self.name,
        "slug": self.slug,
        "lutris_slug": self.lutris_slug,
        "icon": self.icon,
        "logo": self.logo,
        "details": str(self.details),
    }
    existing_game = ServiceGameCollection.get_game(self.service, self.appid)
    if existing_game:
        sql.db_update(PGA_DB, "service_games", game_data, {"id": existing_game["id"]})
    else:
        sql.db_insert(PGA_DB, "service_games", game_data)

service_media

PGA_DB

ServiceMedia

Information about the service's media format

Source code in lutris/services/service_media.py
class ServiceMedia:
    """Information about the service's media format"""

    service = NotImplemented
    size = NotImplemented
    source = "remote"  # set to local if the files don't need to be downloaded
    visible = True  # This media should be displayed as an option in the UI
    small_size = None
    dest_path = None
    file_pattern = NotImplemented
    api_field = NotImplemented
    url_pattern = "%s"

    def __init__(self):
        if self.dest_path and not system.path_exists(self.dest_path):
            os.makedirs(self.dest_path)

    def get_filename(self, slug):
        return self.file_pattern % slug

    def get_absolute_path(self, slug):
        """Return the abolute path of a local media"""
        return os.path.join(self.dest_path, self.get_filename(slug))

    def exists(self, slug):
        """Whether the icon for the specified slug exists locally"""
        return system.path_exists(self.get_absolute_path(slug))

    def get_pixbuf_for_game(self, slug, is_installed=True):
        image_abspath = self.get_absolute_path(slug)
        return get_pixbuf(image_abspath, self.size, fallback=get_default_icon(self.size), is_installed=is_installed)

    def get_media_url(self, details):
        if self.api_field not in details:
            logger.warning("No field '%s' in API game %s", self.api_field, details)
            return
        if not details[self.api_field]:
            return
        return self.url_pattern % details[self.api_field]

    def get_media_urls(self):
        """Return URLs for icons and logos from a service"""
        if self.source == "local":
            return {}
        service_games = ServiceGameCollection.get_for_service(self.service)
        medias = {}
        for game in service_games:
            if not game["details"]:
                continue
            details = json.loads(game["details"])
            media_url = self.get_media_url(details)
            if not media_url:
                continue
            medias[game["slug"]] = media_url
        return medias

    def download(self, slug, url):
        """Downloads the banner if not present"""
        if not url:
            return
        cache_path = os.path.join(self.dest_path, self.get_filename(slug))
        if system.path_exists(cache_path, exclude_empty=True):
            return
        if system.path_exists(cache_path):
            cache_stats = os.stat(cache_path)
            # Empty files have a life time between 1 and 2 weeks, retry them after
            if time.time() - cache_stats.st_mtime < 3600 * 24 * random.choice(range(7, 15)):
                return cache_path
            os.unlink(cache_path)
        try:
            return download_file(url, cache_path, raise_errors=True)
        except HTTPError as ex:
            logger.error("Failed to download %s: %s", url, ex)

    def render(self):
        """Used if the media requires extra processing"""
api_field
dest_path
file_pattern
service
size
small_size
source
url_pattern
visible
__init__(self) special
Source code in lutris/services/service_media.py
def __init__(self):
    if self.dest_path and not system.path_exists(self.dest_path):
        os.makedirs(self.dest_path)
download(self, slug, url)

Downloads the banner if not present

Source code in lutris/services/service_media.py
def download(self, slug, url):
    """Downloads the banner if not present"""
    if not url:
        return
    cache_path = os.path.join(self.dest_path, self.get_filename(slug))
    if system.path_exists(cache_path, exclude_empty=True):
        return
    if system.path_exists(cache_path):
        cache_stats = os.stat(cache_path)
        # Empty files have a life time between 1 and 2 weeks, retry them after
        if time.time() - cache_stats.st_mtime < 3600 * 24 * random.choice(range(7, 15)):
            return cache_path
        os.unlink(cache_path)
    try:
        return download_file(url, cache_path, raise_errors=True)
    except HTTPError as ex:
        logger.error("Failed to download %s: %s", url, ex)
exists(self, slug)

Whether the icon for the specified slug exists locally

Source code in lutris/services/service_media.py
def exists(self, slug):
    """Whether the icon for the specified slug exists locally"""
    return system.path_exists(self.get_absolute_path(slug))
get_absolute_path(self, slug)

Return the abolute path of a local media

Source code in lutris/services/service_media.py
def get_absolute_path(self, slug):
    """Return the abolute path of a local media"""
    return os.path.join(self.dest_path, self.get_filename(slug))
get_filename(self, slug)
Source code in lutris/services/service_media.py
def get_filename(self, slug):
    return self.file_pattern % slug
get_media_url(self, details)
Source code in lutris/services/service_media.py
def get_media_url(self, details):
    if self.api_field not in details:
        logger.warning("No field '%s' in API game %s", self.api_field, details)
        return
    if not details[self.api_field]:
        return
    return self.url_pattern % details[self.api_field]
get_media_urls(self)

Return URLs for icons and logos from a service

Source code in lutris/services/service_media.py
def get_media_urls(self):
    """Return URLs for icons and logos from a service"""
    if self.source == "local":
        return {}
    service_games = ServiceGameCollection.get_for_service(self.service)
    medias = {}
    for game in service_games:
        if not game["details"]:
            continue
        details = json.loads(game["details"])
        media_url = self.get_media_url(details)
        if not media_url:
            continue
        medias[game["slug"]] = media_url
    return medias
get_pixbuf_for_game(self, slug, is_installed=True)
Source code in lutris/services/service_media.py
def get_pixbuf_for_game(self, slug, is_installed=True):
    image_abspath = self.get_absolute_path(slug)
    return get_pixbuf(image_abspath, self.size, fallback=get_default_icon(self.size), is_installed=is_installed)
render(self)

Used if the media requires extra processing

Source code in lutris/services/service_media.py
def render(self):
    """Used if the media requires extra processing"""

steam

Steam service

SteamBanner (ServiceMedia)

Source code in lutris/services/steam.py
class SteamBanner(ServiceMedia):
    service = "steam"
    size = (184, 69)
    dest_path = os.path.join(settings.CACHE_DIR, "steam/banners")
    file_pattern = "%s.jpg"
    api_field = "appid"
    url_pattern = "http://cdn.akamai.steamstatic.com/steam/apps/%s/capsule_184x69.jpg"
api_field
dest_path
file_pattern
service
size
url_pattern

SteamBannerLarge (ServiceMedia)

Source code in lutris/services/steam.py
class SteamBannerLarge(ServiceMedia):
    service = "steam"
    size = (460, 215)
    dest_path = os.path.join(settings.CACHE_DIR, "steam/header")
    file_pattern = "%s.jpg"
    api_field = "appid"
    url_pattern = "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg"
api_field
dest_path
file_pattern
service
size
url_pattern

SteamCover (ServiceMedia)

Source code in lutris/services/steam.py
class SteamCover(ServiceMedia):
    service = "steam"
    size = (200, 300)
    dest_path = os.path.join(settings.CACHE_DIR, "steam/covers")
    file_pattern = "%s.jpg"
    api_field = "appid"
    url_pattern = "http://cdn.steamstatic.com/steam/apps/%s/library_600x900.jpg"
api_field
dest_path
file_pattern
service
size
url_pattern

SteamGame (ServiceGame)

ServiceGame for Steam games

Source code in lutris/services/steam.py
class SteamGame(ServiceGame):
    """ServiceGame for Steam games"""

    service = "steam"
    installer_slug = "steam"
    runner = "steam"

    @classmethod
    def new_from_steam_game(cls, steam_game, game_id=None):
        """Return a Steam game instance from an AppManifest"""
        game = cls()
        game.appid = steam_game["appid"]
        game.game_id = steam_game["appid"]
        game.name = steam_game["name"]
        game.slug = slugify(steam_game["name"])
        game.runner = cls.runner
        game.details = json.dumps(steam_game)
        return game
installer_slug
runner
service
new_from_steam_game(steam_game, game_id=None) classmethod

Return a Steam game instance from an AppManifest

Source code in lutris/services/steam.py
@classmethod
def new_from_steam_game(cls, steam_game, game_id=None):
    """Return a Steam game instance from an AppManifest"""
    game = cls()
    game.appid = steam_game["appid"]
    game.game_id = steam_game["appid"]
    game.name = steam_game["name"]
    game.slug = slugify(steam_game["name"])
    game.runner = cls.runner
    game.details = json.dumps(steam_game)
    return game

SteamService (BaseService)

Source code in lutris/services/steam.py
class SteamService(BaseService):
    id = "steam"
    name = _("Steam")
    icon = "steam-client"
    medias = {
        "banner": SteamBanner,
        "banner_large": SteamBannerLarge,
        "cover": SteamCover,
    }
    default_format = "banner"
    is_loading = False
    runner = "steam"
    excluded_appids = [
        "221410",  # Steam for Linux
        "228980",  # Steamworks Common Redistributables
        "1070560",  # Steam Linux Runtime
    ]
    game_class = SteamGame

    def load(self):
        """Return importable Steam games"""
        if self.is_loading:
            logger.warning("Steam games are already loading")
            return
        self.is_loading = True
        steamid = get_user_steam_id()
        if not steamid:
            logger.error("Unable to find SteamID from Steam config")
            return
        steam_games = get_steam_library(steamid)
        if not steam_games:
            raise RuntimeError(_("Failed to load games. Check that your profile is set to public during the sync."))
        for steam_game in steam_games:
            if steam_game["appid"] in self.excluded_appids:
                continue
            game = self.game_class.new_from_steam_game(steam_game)
            game.save()
        self.match_games()
        self.is_loading = False
        return steam_games

    def get_installer_files(self, installer, installer_file_id):
        steam_uri = "$STEAM:%s:."
        appid = str(installer.script["game"]["appid"])
        return [
            InstallerFile(installer.game_slug, "steam_game", {
                "url": steam_uri % appid,
                "filename": appid
            })
        ]

    def install_from_steam(self, manifest):
        """Create a new Lutris game based on an existing Steam install"""
        if not manifest.is_installed():
            return
        appid = manifest.steamid
        if appid in self.excluded_appids:
            return
        service_game = ServiceGameCollection.get_game(self.id, appid)
        if not service_game:
            return
        lutris_game_id = "%s-%s" % (self.id, appid)
        existing_game = get_game_by_field(lutris_game_id, "installer_slug")
        if existing_game:
            return
        game_config = LutrisConfig().game_level
        game_config["game"]["appid"] = appid
        configpath = write_game_config(lutris_game_id, game_config)
        game_id = add_game(
            name=service_game["name"],
            runner="steam",
            slug=slugify(service_game["name"]),
            installed=1,
            installer_slug=lutris_game_id,
            configpath=configpath,
            platform="Linux",
            service=self.id,
            service_id=appid,
        )
        return game_id

    @property
    def steamapps_paths(self):
        return get_steamapps_paths()

    def add_installed_games(self):
        """Syncs installed Steam games with Lutris"""
        installed_appids = []
        for steamapps_path in self.steamapps_paths:
            for appmanifest_file in get_appmanifests(steamapps_path):
                app_manifest_path = os.path.join(steamapps_path, appmanifest_file)
                app_manifest = AppManifest(app_manifest_path)
                installed_appids.append(app_manifest.steamid)
                self.install_from_steam(app_manifest)

        db_games = get_games(filters={"runner": "steam"})
        for db_game in db_games:
            steam_game = Game(db_game["id"])
            try:
                appid = steam_game.config.game_level["game"]["appid"]
            except KeyError:
                logger.warning("Steam game %s has no AppID")
                continue
            if appid not in installed_appids:
                steam_game.remove(no_signal=True)

        db_appids = defaultdict(list)
        db_games = get_games(filters={"service": "steam"})
        for db_game in db_games:
            db_appids[db_game["service_id"]].append(db_game["id"])

        for appid in db_appids:
            game_ids = db_appids[appid]
            if len(game_ids) == 1:
                continue
            for game_id in game_ids:
                steam_game = Game(game_id)
                if not steam_game.playtime:
                    steam_game.remove(no_signal=True)
                    steam_game.delete()

    def generate_installer(self, db_game):
        """Generate a basic Steam installer"""
        return {
            "name": db_game["name"],
            "version": self.name,
            "slug": slugify(db_game["name"]) + "-" + self.id,
            "game_slug": slugify(db_game["name"]),
            "runner": self.runner,
            "appid": db_game["appid"],
            "script": {
                "game": {"appid": db_game["appid"]}
            }
        }

    def install(self, db_game):
        appid = db_game["appid"]
        db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
        existing_game = self.match_existing_game(db_games, appid)
        if existing_game:
            logger.debug("Found steam game: %s", existing_game)
            game = Game(existing_game.id)
            game.save()
            return
        service_installers = self.get_installers_from_api(appid)
        if not service_installers:
            service_installers = [self.generate_installer(db_game)]
        application = Gio.Application.get_default()
        application.show_installer_window(service_installers, service=self, appid=appid)
default_format
excluded_appids
icon
id
is_loading
medias
name
runner
steamapps_paths property readonly
game_class (ServiceGame)

ServiceGame for Steam games

Source code in lutris/services/steam.py
class SteamGame(ServiceGame):
    """ServiceGame for Steam games"""

    service = "steam"
    installer_slug = "steam"
    runner = "steam"

    @classmethod
    def new_from_steam_game(cls, steam_game, game_id=None):
        """Return a Steam game instance from an AppManifest"""
        game = cls()
        game.appid = steam_game["appid"]
        game.game_id = steam_game["appid"]
        game.name = steam_game["name"]
        game.slug = slugify(steam_game["name"])
        game.runner = cls.runner
        game.details = json.dumps(steam_game)
        return game
installer_slug
runner
service
new_from_steam_game(steam_game, game_id=None) classmethod

Return a Steam game instance from an AppManifest

Source code in lutris/services/steam.py
@classmethod
def new_from_steam_game(cls, steam_game, game_id=None):
    """Return a Steam game instance from an AppManifest"""
    game = cls()
    game.appid = steam_game["appid"]
    game.game_id = steam_game["appid"]
    game.name = steam_game["name"]
    game.slug = slugify(steam_game["name"])
    game.runner = cls.runner
    game.details = json.dumps(steam_game)
    return game
add_installed_games(self)

Syncs installed Steam games with Lutris

Source code in lutris/services/steam.py
def add_installed_games(self):
    """Syncs installed Steam games with Lutris"""
    installed_appids = []
    for steamapps_path in self.steamapps_paths:
        for appmanifest_file in get_appmanifests(steamapps_path):
            app_manifest_path = os.path.join(steamapps_path, appmanifest_file)
            app_manifest = AppManifest(app_manifest_path)
            installed_appids.append(app_manifest.steamid)
            self.install_from_steam(app_manifest)

    db_games = get_games(filters={"runner": "steam"})
    for db_game in db_games:
        steam_game = Game(db_game["id"])
        try:
            appid = steam_game.config.game_level["game"]["appid"]
        except KeyError:
            logger.warning("Steam game %s has no AppID")
            continue
        if appid not in installed_appids:
            steam_game.remove(no_signal=True)

    db_appids = defaultdict(list)
    db_games = get_games(filters={"service": "steam"})
    for db_game in db_games:
        db_appids[db_game["service_id"]].append(db_game["id"])

    for appid in db_appids:
        game_ids = db_appids[appid]
        if len(game_ids) == 1:
            continue
        for game_id in game_ids:
            steam_game = Game(game_id)
            if not steam_game.playtime:
                steam_game.remove(no_signal=True)
                steam_game.delete()
generate_installer(self, db_game)

Generate a basic Steam installer

Source code in lutris/services/steam.py
def generate_installer(self, db_game):
    """Generate a basic Steam installer"""
    return {
        "name": db_game["name"],
        "version": self.name,
        "slug": slugify(db_game["name"]) + "-" + self.id,
        "game_slug": slugify(db_game["name"]),
        "runner": self.runner,
        "appid": db_game["appid"],
        "script": {
            "game": {"appid": db_game["appid"]}
        }
    }
get_installer_files(self, installer, installer_file_id)
Source code in lutris/services/steam.py
def get_installer_files(self, installer, installer_file_id):
    steam_uri = "$STEAM:%s:."
    appid = str(installer.script["game"]["appid"])
    return [
        InstallerFile(installer.game_slug, "steam_game", {
            "url": steam_uri % appid,
            "filename": appid
        })
    ]
install(self, db_game)

Install a service game, or starts the installer of the game.

Parameters:

Name Type Description Default
db_game dict or str

Database fields of the game to add, or (for Lutris service only the slug of the game.)

required

Returns:

Type Description
str

The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None.

Source code in lutris/services/steam.py
def install(self, db_game):
    appid = db_game["appid"]
    db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
    existing_game = self.match_existing_game(db_games, appid)
    if existing_game:
        logger.debug("Found steam game: %s", existing_game)
        game = Game(existing_game.id)
        game.save()
        return
    service_installers = self.get_installers_from_api(appid)
    if not service_installers:
        service_installers = [self.generate_installer(db_game)]
    application = Gio.Application.get_default()
    application.show_installer_window(service_installers, service=self, appid=appid)
install_from_steam(self, manifest)

Create a new Lutris game based on an existing Steam install

Source code in lutris/services/steam.py
def install_from_steam(self, manifest):
    """Create a new Lutris game based on an existing Steam install"""
    if not manifest.is_installed():
        return
    appid = manifest.steamid
    if appid in self.excluded_appids:
        return
    service_game = ServiceGameCollection.get_game(self.id, appid)
    if not service_game:
        return
    lutris_game_id = "%s-%s" % (self.id, appid)
    existing_game = get_game_by_field(lutris_game_id, "installer_slug")
    if existing_game:
        return
    game_config = LutrisConfig().game_level
    game_config["game"]["appid"] = appid
    configpath = write_game_config(lutris_game_id, game_config)
    game_id = add_game(
        name=service_game["name"],
        runner="steam",
        slug=slugify(service_game["name"]),
        installed=1,
        installer_slug=lutris_game_id,
        configpath=configpath,
        platform="Linux",
        service=self.id,
        service_id=appid,
    )
    return game_id
load(self)

Return importable Steam games

Source code in lutris/services/steam.py
def load(self):
    """Return importable Steam games"""
    if self.is_loading:
        logger.warning("Steam games are already loading")
        return
    self.is_loading = True
    steamid = get_user_steam_id()
    if not steamid:
        logger.error("Unable to find SteamID from Steam config")
        return
    steam_games = get_steam_library(steamid)
    if not steam_games:
        raise RuntimeError(_("Failed to load games. Check that your profile is set to public during the sync."))
    for steam_game in steam_games:
        if steam_game["appid"] in self.excluded_appids:
            continue
        game = self.game_class.new_from_steam_game(steam_game)
        game.save()
    self.match_games()
    self.is_loading = False
    return steam_games

steamwindows

STEAM_INSTALLER

SteamWindowsGame (SteamGame)

Source code in lutris/services/steamwindows.py
class SteamWindowsGame(SteamGame):
    service = "steamwindows"
    installer_slug = "steamwindows"
    runner = "wine"
installer_slug
runner
service

SteamWindowsService (SteamService)

Source code in lutris/services/steamwindows.py
class SteamWindowsService(SteamService):
    id = "steamwindows"
    name = _("Steam for Windows")
    runner = "wine"
    game_class = SteamWindowsGame
    client_installer = "steam-wine"

    def generate_installer(self, db_game, steam_game):
        """Generate a basic Steam installer"""
        return {
            "name": db_game["name"],
            "version": self.name,
            "slug": slugify(db_game["name"]) + "-" + self.id,
            "game_slug": slugify(db_game["name"]),
            "runner": self.runner,
            "appid": db_game["appid"],
            "script": {
                "requires": self.client_installer,
                "game": {
                    "exe": steam_game.config.game_config["exe"],
                    "args": "-no-cef-sandbox -applaunch %s" % db_game["appid"],
                    "prefix": steam_game.config.game_config["prefix"],
                }
            }
        }

    def get_steam(self):
        db_entry = get_game_by_field(self.client_installer, "installer_slug")
        if db_entry:
            return Game(db_entry["id"])

    def install(self, db_game):
        steam_game = self.get_steam()
        if not steam_game:
            installers = get_installers(
                game_slug=self.client_installer,
            )
            appid = None
        else:
            installers = [self.generate_installer(db_game, steam_game)]
            appid = db_game["appid"]
            db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
            existing_game = self.match_existing_game(db_games, appid)
            if existing_game:
                logger.debug("Found steam game: %s", existing_game)
                game = Game(existing_game.id)
                game.save()
                return
        application = Gio.Application.get_default()
        application.show_installer_window(
            installers,
            service=self,
            appid=appid
        )

    @property
    def steamapps_paths(self):
        """Return steamapps paths"""
        steam_game = self.get_steam()
        if not steam_game:
            return []
        dirs = []
        steam_path = steam_game.config.game_config["exe"]
        steam_data_dir = os.path.dirname(steam_path)
        if steam_data_dir:
            main_dir = os.path.join(steam_data_dir, "steamapps")
            main_dir = system.fix_path_case(main_dir)
            if main_dir and os.path.isdir(main_dir):
                dirs.append(os.path.abspath(main_dir))
        return dirs
client_installer
id
name
runner
steamapps_paths property readonly

Return steamapps paths

game_class (SteamGame)
Source code in lutris/services/steamwindows.py
class SteamWindowsGame(SteamGame):
    service = "steamwindows"
    installer_slug = "steamwindows"
    runner = "wine"
installer_slug
runner
service
generate_installer(self, db_game, steam_game)

Generate a basic Steam installer

Source code in lutris/services/steamwindows.py
def generate_installer(self, db_game, steam_game):
    """Generate a basic Steam installer"""
    return {
        "name": db_game["name"],
        "version": self.name,
        "slug": slugify(db_game["name"]) + "-" + self.id,
        "game_slug": slugify(db_game["name"]),
        "runner": self.runner,
        "appid": db_game["appid"],
        "script": {
            "requires": self.client_installer,
            "game": {
                "exe": steam_game.config.game_config["exe"],
                "args": "-no-cef-sandbox -applaunch %s" % db_game["appid"],
                "prefix": steam_game.config.game_config["prefix"],
            }
        }
    }
get_steam(self)
Source code in lutris/services/steamwindows.py
def get_steam(self):
    db_entry = get_game_by_field(self.client_installer, "installer_slug")
    if db_entry:
        return Game(db_entry["id"])
install(self, db_game)

Install a service game, or starts the installer of the game.

Parameters:

Name Type Description Default
db_game dict or str

Database fields of the game to add, or (for Lutris service only the slug of the game.)

required

Returns:

Type Description
str

The slug of the game that was installed, to be run. None if the game should not be run now. Many installers start from here, but continue running after this returns; they return None.

Source code in lutris/services/steamwindows.py
def install(self, db_game):
    steam_game = self.get_steam()
    if not steam_game:
        installers = get_installers(
            game_slug=self.client_installer,
        )
        appid = None
    else:
        installers = [self.generate_installer(db_game, steam_game)]
        appid = db_game["appid"]
        db_games = get_games(filters={"service_id": appid, "installed": "1", "service": self.id})
        existing_game = self.match_existing_game(db_games, appid)
        if existing_game:
            logger.debug("Found steam game: %s", existing_game)
            game = Game(existing_game.id)
            game.save()
            return
    application = Gio.Application.get_default()
    application.show_installer_window(
        installers,
        service=self,
        appid=appid
    )

tosec

TOSEC service Not ready yet

TOSECService (BaseService)

Service class for TOSEC

Source code in lutris/services/tosec.py
class TOSECService(BaseService):
    """Service class for TOSEC"""
    id = "tosec"
    name = _("TOSEC")
    icon = "tosec"
icon
id
name

ubisoft

Ubisoft Connect service

UbisoftConnectService (OnlineService)

Service class for Ubisoft Connect

Source code in lutris/services/ubisoft.py
class UbisoftConnectService(OnlineService):
    """Service class for Ubisoft Connect"""
    id = "ubisoft"
    name = _("Ubisoft Connect")
    icon = "ubisoft"
    runner = "wine"
    client_installer = "ubisoft-connect"
    browser_size = (460, 690)
    cookies_path = os.path.join(settings.CACHE_DIR, "ubisoft/.auth")
    token_path = os.path.join(settings.CACHE_DIR, "ubisoft/.token")
    cache_path = os.path.join(settings.CACHE_DIR, "ubisoft/library/")
    login_url = consts.LOGIN_URL
    redirect_uri = "https://connect.ubisoft.com/change_domain/"
    scripts = {
        "https://connect.ubisoft.com/ready": (
            'window.location.replace("https://connect.ubisoft.com/change_domain/");'
        ),
        "https://connect.ubisoft.com/change_domain/": (
            'window.location.replace(localStorage.getItem("PRODloginData") +","+ '
            'localStorage.getItem("PRODrememberMe") +"," + localStorage.getItem("PRODlastProfile"));'
        )
    }
    medias = {
        "cover": UbisoftCover,
    }
    default_format = "cover"
    is_loading = False

    def __init__(self):
        super().__init__()
        self.client = UbisoftConnectClient(self)

    def auth_lost(self):
        self.emit("service-logout")

    def login_callback(self, credentials):
        """Called after the user has logged in successfully"""
        url = credentials[len("https://connect.ubisoft.com/change_domain/"):]
        unquoted_url = unquote(url)
        storage_jsons = json.loads("[" + unquoted_url + "]")
        user_data = self.client.authorise_with_local_storage(storage_jsons)
        self.client.set_auth_lost_callback(self.auth_lost)
        self.emit("service-login")
        return (user_data['userId'], user_data['username'])

    def run(self):
        db_game = get_game_by_field(self.client_installer, "slug")
        game = Game(db_game["id"])
        game.emit("game-launch")

    def is_launchable(self):
        return get_game_by_field(self.client_installer, "slug")

    def is_connected(self):
        return self.is_authenticated()

    def get_configurations(self):
        ubi_game = get_game_by_field("ubisoft-connect", "slug")
        if not ubi_game:
            return
        base_dir = ubi_game["directory"]
        configurations_path = os.path.join(
            base_dir,
            "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/"
            "cache/configuration/configurations"
        )
        with open(configurations_path, "rb") as config_file:
            content = config_file.read()
        return content

    def load(self):
        self.is_loading = True
        self.client.authorise_with_stored_credentials(self.load_credentials())
        response = self.client.get_club_titles()
        games = response['data']['viewer']['ownedGames'].get('nodes', [])
        ubi_games = []
        for game in games:
            if "ownedPlatformGroups" in game:
                is_pc = False
                for platform_group in game["ownedPlatformGroups"]:
                    for platform in platform_group:
                        if platform["type"] == "PC":
                            is_pc = True
                if not is_pc:
                    continue
            ubi_game = UbisoftGame.new_from_api(game)
            ubi_game.save()
            ubi_games.append(ubi_game)
        configuration_data = self.get_configurations()
        config_parser = UbisoftParser()
        games = []
        for game in config_parser.parse_games(configuration_data):
            ubi_game = UbisoftGame.new_from_api(game)
            ubi_game.save()
            ubi_games.append(ubi_game)
        self.is_loading = False
        return ubi_games

    def store_credentials(self, credentials):
        with open(self.token_path, "w", encoding='utf-8') as auth_file:
            auth_file.write(json.dumps(credentials, indent=2))

    def load_credentials(self):
        with open(self.token_path) as auth_file:
            credentials = json.load(auth_file)
        return credentials

    def install_from_ubisoft(self, ubisoft_connect, game):
        app_name = game["name"]

        lutris_game_id = slugify(game["name"]) + "-" + self.id
        existing_game = get_game_by_field(lutris_game_id, "installer_slug")
        if existing_game:
            logger.debug("Ubisoft Connect game %s is already installed", app_name)
            return
        logger.debug("Installing Ubisoft Connect game %s", app_name)
        game_config = LutrisConfig(game_config_id=ubisoft_connect["configpath"]).game_level
        game_config["game"]["args"] = f"uplay://launch/{game['appid']}"
        configpath = write_game_config(lutris_game_id, game_config)
        game_id = add_game(
            name=game["name"],
            runner=self.runner,
            slug=slugify(game["name"]),
            directory=ubisoft_connect["directory"],
            installed=1,
            installer_slug=lutris_game_id,
            configpath=configpath,
            service=self.id,
            service_id=game["appid"],
        )
        return game_id

    def add_installed_games(self):
        ubisoft_connect = get_game_by_field(self.client_installer, "slug")
        if not ubisoft_connect:
            logger.warning("Ubisoft Connect not installed")
            return
        prefix_path = ubisoft_connect["directory"].split("drive_c")[0]
        prefix = WinePrefixManager(prefix_path)
        for game in ServiceGameCollection.get_for_service(self.id):
            details = json.loads(game["details"])
            install_path = get_ubisoft_registry(prefix, details.get("registryPath"))
            exe = get_ubisoft_registry(prefix, details.get("exe"))
            if install_path and exe:
                self.install_from_ubisoft(ubisoft_connect, game)

    def generate_installer(self, db_game, ubi_db_game):
        ubisoft_connect = Game(ubi_db_game["id"])
        uc_exe = ubisoft_connect.config.game_config["exe"]
        if not os.path.isabs(uc_exe):
            uc_exe = os.path.join(ubisoft_connect.config.game_config["prefix"], uc_exe)
        return {
            "name": db_game["name"],
            "version": self.name,
            "slug": slugify(db_game["name"]) + "-" + self.id,
            "game_slug": slugify(db_game["name"]),
            "runner": self.runner,
            "appid": db_game["appid"],
            "script": {
                "requires": self.client_installer,
                "game": {
                    "args": f"uplay://launch/{db_game['appid']}",
                },
                "installer": [
                    {"task": {
                        "name": "wineexec",
                        "executable": uc_exe,
                        "args": f"uplay://install/{db_game['appid']}",
                        "prefix": ubisoft_connect.config.game_config["prefix"],
                        "description": (
                            "Ubisoft will now open and install %s. "
                            "Close Ubisoft Connect to complete the install process."
                        ) % db_game["name"]
                    }}
                ]
            }
        }

    def install(self, db_game):
        """Install a game or Ubisoft Connect if not already installed"""
        ubisoft_connect = get_game_by_field(self.client_installer, "slug")
        application = Gio.Application.get_default()
        if not ubisoft_connect or not ubisoft_connect["installed"]:
            logger.warning("Ubisoft Connect (%s) not installed", self.client_installer)
            installers = get_installers(game_slug=self.client_installer)
            application.show_installer_window(installers)
        else:
            application.show_installer_window(
                [self.generate_installer(db_game, ubisoft_connect)],
                service=self,
                appid=db_game["appid"]
            )
browser_size
cache_path
client_installer
cookies_path
default_format
icon
id
is_loading
login_url
medias
name
redirect_uri
runner
scripts
token_path
__init__(self) special
Source code in lutris/services/ubisoft.py
def __init__(self):
    super().__init__()
    self.client = UbisoftConnectClient(self)
add_installed_games(self)

Services can implement this method to scan for locally installed games and add them to lutris.

Source code in lutris/services/ubisoft.py
def add_installed_games(self):
    ubisoft_connect = get_game_by_field(self.client_installer, "slug")
    if not ubisoft_connect:
        logger.warning("Ubisoft Connect not installed")
        return
    prefix_path = ubisoft_connect["directory"].split("drive_c")[0]
    prefix = WinePrefixManager(prefix_path)
    for game in ServiceGameCollection.get_for_service(self.id):
        details = json.loads(game["details"])
        install_path = get_ubisoft_registry(prefix, details.get("registryPath"))
        exe = get_ubisoft_registry(prefix, details.get("exe"))
        if install_path and exe:
            self.install_from_ubisoft(ubisoft_connect, game)
auth_lost(self)
Source code in lutris/services/ubisoft.py
def auth_lost(self):
    self.emit("service-logout")
generate_installer(self, db_game, ubi_db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/ubisoft.py
def generate_installer(self, db_game, ubi_db_game):
    ubisoft_connect = Game(ubi_db_game["id"])
    uc_exe = ubisoft_connect.config.game_config["exe"]
    if not os.path.isabs(uc_exe):
        uc_exe = os.path.join(ubisoft_connect.config.game_config["prefix"], uc_exe)
    return {
        "name": db_game["name"],
        "version": self.name,
        "slug": slugify(db_game["name"]) + "-" + self.id,
        "game_slug": slugify(db_game["name"]),
        "runner": self.runner,
        "appid": db_game["appid"],
        "script": {
            "requires": self.client_installer,
            "game": {
                "args": f"uplay://launch/{db_game['appid']}",
            },
            "installer": [
                {"task": {
                    "name": "wineexec",
                    "executable": uc_exe,
                    "args": f"uplay://install/{db_game['appid']}",
                    "prefix": ubisoft_connect.config.game_config["prefix"],
                    "description": (
                        "Ubisoft will now open and install %s. "
                        "Close Ubisoft Connect to complete the install process."
                    ) % db_game["name"]
                }}
            ]
        }
    }
get_configurations(self)
Source code in lutris/services/ubisoft.py
def get_configurations(self):
    ubi_game = get_game_by_field("ubisoft-connect", "slug")
    if not ubi_game:
        return
    base_dir = ubi_game["directory"]
    configurations_path = os.path.join(
        base_dir,
        "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/"
        "cache/configuration/configurations"
    )
    with open(configurations_path, "rb") as config_file:
        content = config_file.read()
    return content
install(self, db_game)

Install a game or Ubisoft Connect if not already installed

Source code in lutris/services/ubisoft.py
def install(self, db_game):
    """Install a game or Ubisoft Connect if not already installed"""
    ubisoft_connect = get_game_by_field(self.client_installer, "slug")
    application = Gio.Application.get_default()
    if not ubisoft_connect or not ubisoft_connect["installed"]:
        logger.warning("Ubisoft Connect (%s) not installed", self.client_installer)
        installers = get_installers(game_slug=self.client_installer)
        application.show_installer_window(installers)
    else:
        application.show_installer_window(
            [self.generate_installer(db_game, ubisoft_connect)],
            service=self,
            appid=db_game["appid"]
        )
install_from_ubisoft(self, ubisoft_connect, game)
Source code in lutris/services/ubisoft.py
def install_from_ubisoft(self, ubisoft_connect, game):
    app_name = game["name"]

    lutris_game_id = slugify(game["name"]) + "-" + self.id
    existing_game = get_game_by_field(lutris_game_id, "installer_slug")
    if existing_game:
        logger.debug("Ubisoft Connect game %s is already installed", app_name)
        return
    logger.debug("Installing Ubisoft Connect game %s", app_name)
    game_config = LutrisConfig(game_config_id=ubisoft_connect["configpath"]).game_level
    game_config["game"]["args"] = f"uplay://launch/{game['appid']}"
    configpath = write_game_config(lutris_game_id, game_config)
    game_id = add_game(
        name=game["name"],
        runner=self.runner,
        slug=slugify(game["name"]),
        directory=ubisoft_connect["directory"],
        installed=1,
        installer_slug=lutris_game_id,
        configpath=configpath,
        service=self.id,
        service_id=game["appid"],
    )
    return game_id
is_connected(self)
Source code in lutris/services/ubisoft.py
def is_connected(self):
    return self.is_authenticated()
is_launchable(self)
Source code in lutris/services/ubisoft.py
def is_launchable(self):
    return get_game_by_field(self.client_installer, "slug")
load(self)
Source code in lutris/services/ubisoft.py
def load(self):
    self.is_loading = True
    self.client.authorise_with_stored_credentials(self.load_credentials())
    response = self.client.get_club_titles()
    games = response['data']['viewer']['ownedGames'].get('nodes', [])
    ubi_games = []
    for game in games:
        if "ownedPlatformGroups" in game:
            is_pc = False
            for platform_group in game["ownedPlatformGroups"]:
                for platform in platform_group:
                    if platform["type"] == "PC":
                        is_pc = True
            if not is_pc:
                continue
        ubi_game = UbisoftGame.new_from_api(game)
        ubi_game.save()
        ubi_games.append(ubi_game)
    configuration_data = self.get_configurations()
    config_parser = UbisoftParser()
    games = []
    for game in config_parser.parse_games(configuration_data):
        ubi_game = UbisoftGame.new_from_api(game)
        ubi_game.save()
        ubi_games.append(ubi_game)
    self.is_loading = False
    return ubi_games
load_credentials(self)
Source code in lutris/services/ubisoft.py
def load_credentials(self):
    with open(self.token_path) as auth_file:
        credentials = json.load(auth_file)
    return credentials
login_callback(self, credentials)

Called after the user has logged in successfully

Source code in lutris/services/ubisoft.py
def login_callback(self, credentials):
    """Called after the user has logged in successfully"""
    url = credentials[len("https://connect.ubisoft.com/change_domain/"):]
    unquoted_url = unquote(url)
    storage_jsons = json.loads("[" + unquoted_url + "]")
    user_data = self.client.authorise_with_local_storage(storage_jsons)
    self.client.set_auth_lost_callback(self.auth_lost)
    self.emit("service-login")
    return (user_data['userId'], user_data['username'])
run(self)

Override this method to run a launcher

Source code in lutris/services/ubisoft.py
def run(self):
    db_game = get_game_by_field(self.client_installer, "slug")
    game = Game(db_game["id"])
    game.emit("game-launch")
store_credentials(self, credentials)
Source code in lutris/services/ubisoft.py
def store_credentials(self, credentials):
    with open(self.token_path, "w", encoding='utf-8') as auth_file:
        auth_file.write(json.dumps(credentials, indent=2))

UbisoftCover (ServiceMedia)

Ubisoft connect cover art

Source code in lutris/services/ubisoft.py
class UbisoftCover(ServiceMedia):
    """Ubisoft connect cover art"""
    service = "ubisoft"
    size = (160, 186)
    dest_path = os.path.join(settings.CACHE_DIR, "ubisoft/covers")
    file_pattern = "%s.jpg"
    api_field = "id"
    url_pattern = "https://ubiservices.cdn.ubi.com/%s/spaceCardAsset/boxArt_mobile.jpg?imwidth=320"

    def get_media_url(self, details):
        if self.api_field in details:
            return super().get_media_url(details)
        return details["thumbImage"]

    def download(self, slug, url):
        if url.startswith("http"):
            return super().download(slug, url)
        if not url.endswith(".jpg"):
            return
        ubi_game = get_game_by_field("ubisoft-connect", "slug")
        if not ubi_game:
            return
        base_dir = ubi_game["directory"]
        asset_file = os.path.join(
            base_dir,
            "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/cache/assets",
            url
        )
        cache_path = os.path.join(self.dest_path, self.get_filename(slug))
        if os.path.exists(asset_file):
            shutil.copy(asset_file, cache_path)
        else:
            logger.warning("No thumbnail in %s", asset_file)
api_field
dest_path
file_pattern
service
size
url_pattern
download(self, slug, url)

Downloads the banner if not present

Source code in lutris/services/ubisoft.py
def download(self, slug, url):
    if url.startswith("http"):
        return super().download(slug, url)
    if not url.endswith(".jpg"):
        return
    ubi_game = get_game_by_field("ubisoft-connect", "slug")
    if not ubi_game:
        return
    base_dir = ubi_game["directory"]
    asset_file = os.path.join(
        base_dir,
        "drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/cache/assets",
        url
    )
    cache_path = os.path.join(self.dest_path, self.get_filename(slug))
    if os.path.exists(asset_file):
        shutil.copy(asset_file, cache_path)
    else:
        logger.warning("No thumbnail in %s", asset_file)
get_media_url(self, details)
Source code in lutris/services/ubisoft.py
def get_media_url(self, details):
    if self.api_field in details:
        return super().get_media_url(details)
    return details["thumbImage"]

UbisoftGame (ServiceGame)

Service game for games from Ubisoft connect

Source code in lutris/services/ubisoft.py
class UbisoftGame(ServiceGame):
    """Service game for games from Ubisoft connect"""
    service = "ubisoft"

    @classmethod
    def new_from_api(cls, payload):
        """Convert an Ubisoft game to a service game"""
        service_game = cls()
        service_game.appid = payload["spaceId"] or payload["installId"]
        service_game.slug = slugify(payload["name"])
        service_game.name = payload["name"]
        service_game.details = json.dumps(payload)
        return service_game
service
new_from_api(payload) classmethod

Convert an Ubisoft game to a service game

Source code in lutris/services/ubisoft.py
@classmethod
def new_from_api(cls, payload):
    """Convert an Ubisoft game to a service game"""
    service_game = cls()
    service_game.appid = payload["spaceId"] or payload["installId"]
    service_game.slug = slugify(payload["name"])
    service_game.name = payload["name"]
    service_game.details = json.dumps(payload)
    return service_game

xdg

XDG applications service

XDGGame (ServiceGame)

XDG game (Linux game with a desktop launcher)

Source code in lutris/services/xdg.py
class XDGGame(ServiceGame):
    """XDG game (Linux game with a desktop launcher)"""

    service = "xdg"
    runner = "linux"
    installer_slug = "desktopapp"

    @staticmethod
    def get_app_icon(xdg_app):
        """Return the name of the icon for an XDG app if one if set"""
        icon = xdg_app.get_icon()
        if not icon:
            return ""
        return icon.to_string()

    @classmethod
    def new_from_xdg_app(cls, xdg_app):
        """Create a service game from a XDG entry"""
        service_game = cls()
        service_game.name = xdg_app.get_display_name()
        service_game.icon = cls.get_app_icon(xdg_app)
        service_game.appid = get_appid(xdg_app)
        service_game.slug = cls.get_slug(xdg_app)
        exe, args = cls.get_command_args(xdg_app)
        service_game.details = json.dumps({
            "exe": exe,
            "args": args,
        })
        return service_game

    @staticmethod
    def get_command_args(app):
        """Return a tuple with absolute command path and an argument string"""
        command = shlex.split(app.get_commandline())
        # remove %U etc. and change %% to % in arguments
        args = list(map(lambda arg: re.sub("%[^%]", "", arg).replace("%%", "%"), command[1:]))
        exe = command[0]
        if not exe.startswith("/"):
            exe = system.find_executable(exe)
        return exe, subprocess.list2cmdline(args)

    @staticmethod
    def get_slug(xdg_app):
        """Get the slug from the game name"""
        return slugify(xdg_app.get_display_name()) or slugify(get_appid(xdg_app))
installer_slug
runner
service
get_app_icon(xdg_app) staticmethod

Return the name of the icon for an XDG app if one if set

Source code in lutris/services/xdg.py
@staticmethod
def get_app_icon(xdg_app):
    """Return the name of the icon for an XDG app if one if set"""
    icon = xdg_app.get_icon()
    if not icon:
        return ""
    return icon.to_string()
get_command_args(app) staticmethod

Return a tuple with absolute command path and an argument string

Source code in lutris/services/xdg.py
@staticmethod
def get_command_args(app):
    """Return a tuple with absolute command path and an argument string"""
    command = shlex.split(app.get_commandline())
    # remove %U etc. and change %% to % in arguments
    args = list(map(lambda arg: re.sub("%[^%]", "", arg).replace("%%", "%"), command[1:]))
    exe = command[0]
    if not exe.startswith("/"):
        exe = system.find_executable(exe)
    return exe, subprocess.list2cmdline(args)
get_slug(xdg_app) staticmethod

Get the slug from the game name

Source code in lutris/services/xdg.py
@staticmethod
def get_slug(xdg_app):
    """Get the slug from the game name"""
    return slugify(xdg_app.get_display_name()) or slugify(get_appid(xdg_app))
new_from_xdg_app(xdg_app) classmethod

Create a service game from a XDG entry

Source code in lutris/services/xdg.py
@classmethod
def new_from_xdg_app(cls, xdg_app):
    """Create a service game from a XDG entry"""
    service_game = cls()
    service_game.name = xdg_app.get_display_name()
    service_game.icon = cls.get_app_icon(xdg_app)
    service_game.appid = get_appid(xdg_app)
    service_game.slug = cls.get_slug(xdg_app)
    exe, args = cls.get_command_args(xdg_app)
    service_game.details = json.dumps({
        "exe": exe,
        "args": args,
    })
    return service_game

XDGMedia (ServiceMedia)

Source code in lutris/services/xdg.py
class XDGMedia(ServiceMedia):
    service = "xdg"
    source = "local"
    size = (64, 64)
    dest_path = os.path.join(settings.CACHE_DIR, "xdg/icons")
    file_pattern = "%s.png"
dest_path
file_pattern
service
size
source

XDGService (BaseService)

Source code in lutris/services/xdg.py
class XDGService(BaseService):
    id = "xdg"
    name = _("Local")
    icon = "linux"
    online = False
    local = True
    medias = {
        "icon": XDGMedia
    }

    ignored_games = ("lutris", )
    ignored_executables = ("lutris", "steam")
    ignored_categories = ("Emulator", "Development", "Utility")

    @classmethod
    def iter_xdg_games(cls):
        """Iterates through XDG games only"""
        for app in Gio.AppInfo.get_all():
            if cls._is_importable(app):
                yield app

    @property
    def lutris_games(self):
        """Iterates through Lutris games imported from XDG"""
        for game in get_games_where(runner=XDGGame.runner, installer_slug=XDGGame.installer_slug, installed=1):
            yield game

    @classmethod
    def _is_importable(cls, app):
        """Returns whether a XDG game is importable to Lutris"""
        appid = get_appid(app)
        executable = app.get_executable() or ""
        if any(
            [
                app.get_nodisplay() or app.get_is_hidden(),  # App is hidden
                not executable,  # Check app has an executable
                appid.startswith("net.lutris"),  # Skip lutris created shortcuts
                appid.lower() in map(str.lower, cls.ignored_games),  # game blacklisted
                executable.lower() in cls.ignored_executables,  # exe blacklisted
            ]
        ):
            return False

        # must be in Game category
        categories = app.get_categories() or ""
        categories = list(filter(None, categories.lower().split(";")))
        if "game" not in categories:
            return False

        # contains a blacklisted category
        if bool([category for category in categories if category in map(str.lower, cls.ignored_categories)]):
            return False
        return True

    def match_games(self):
        """XDG games aren't on the lutris website"""
        return

    def load(self):
        """Return the list of games stored in the XDG menu."""
        xdg_games = [XDGGame.new_from_xdg_app(app) for app in self.iter_xdg_games()]
        for game in xdg_games:
            game.save()
        return xdg_games

    def generate_installer(self, db_game):
        details = json.loads(db_game["details"])
        return {
            "name": db_game["name"],
            "version": "XDG",
            "slug": db_game["slug"],
            "game_slug": slugify(db_game["name"]),
            "runner": "linux",
            "script": {
                "game": {
                    "exe": details["exe"],
                    "args": details["args"],
                },
                "system": {"disable_runtime": True}
            }
        }

    def get_game_directory(self, installer):
        """Pull install location from installer"""
        return os.path.dirname(installer["script"]["game"]["exe"])
icon
id
ignored_categories
ignored_executables
ignored_games
local
lutris_games property readonly

Iterates through Lutris games imported from XDG

medias
name
online
generate_installer(self, db_game)

Used to generate an installer from the data returned from the services

Source code in lutris/services/xdg.py
def generate_installer(self, db_game):
    details = json.loads(db_game["details"])
    return {
        "name": db_game["name"],
        "version": "XDG",
        "slug": db_game["slug"],
        "game_slug": slugify(db_game["name"]),
        "runner": "linux",
        "script": {
            "game": {
                "exe": details["exe"],
                "args": details["args"],
            },
            "system": {"disable_runtime": True}
        }
    }
get_game_directory(self, installer)

Pull install location from installer

Source code in lutris/services/xdg.py
def get_game_directory(self, installer):
    """Pull install location from installer"""
    return os.path.dirname(installer["script"]["game"]["exe"])
iter_xdg_games() classmethod

Iterates through XDG games only

Source code in lutris/services/xdg.py
@classmethod
def iter_xdg_games(cls):
    """Iterates through XDG games only"""
    for app in Gio.AppInfo.get_all():
        if cls._is_importable(app):
            yield app
load(self)

Return the list of games stored in the XDG menu.

Source code in lutris/services/xdg.py
def load(self):
    """Return the list of games stored in the XDG menu."""
    xdg_games = [XDGGame.new_from_xdg_app(app) for app in self.iter_xdg_games()]
    for game in xdg_games:
        game.save()
    return xdg_games
match_games(self)

XDG games aren't on the lutris website

Source code in lutris/services/xdg.py
def match_games(self):
    """XDG games aren't on the lutris website"""
    return

get_appid(app)

Get the appid for the game

Source code in lutris/services/xdg.py
def get_appid(app):
    """Get the appid for the game"""
    try:
        return os.path.splitext(app.get_id())[0]
    except UnicodeDecodeError:
        logger.exception(
            "Failed to read ID for app %s (non UTF-8 encoding). Reverting to executable name.",
            app,
        )
        return app.get_executable()

settings

Internal settings.

AUTHORS

BANNER_PATH

CACHE_DIR

CONFIG_DIR

CONFIG_FILE

COPYRIGHT

COVERART_PATH

DATA_DIR

DISCORD_CLIENT_ID

DRIVER_HOWTO_URL

GAME_CONFIG_DIR

GAME_URL

ICON_PATH

INSTALLER_REVISION_URL

INSTALLER_URL

PROJECT

RUNNER_DIR

RUNTIME_DIR

RUNTIME_URL

SHADER_CACHE_DIR

SHOW_MEDIA

SITE_URL

STEAM_API_KEY

TMP_PATH

VERSION

read_setting

sio

write_setting

startup

Check to run at program start

check_driver()

Report on the currently running driver

Source code in lutris/startup.py
def check_driver():
    """Report on the currently running driver"""
    driver_info = {}
    if drivers.is_nvidia():
        driver_info = drivers.get_nvidia_driver_info()
        # pylint: disable=logging-format-interpolation
        logger.info("Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"]))
        gpus = drivers.get_nvidia_gpu_ids()
        for gpu_id in gpus:
            gpu_info = drivers.get_nvidia_gpu_info(gpu_id)
            logger.info("GPU: %s", gpu_info.get("Model"))
    elif LINUX_SYSTEM.glxinfo:
        # pylint: disable=no-member
        if hasattr(LINUX_SYSTEM.glxinfo, "GLX_MESA_query_renderer"):
            logger.info(
                "Running %s Mesa driver %s on %s",
                LINUX_SYSTEM.glxinfo.opengl_vendor,
                LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version,
                LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.device,
            )
    else:
        logger.warning("glxinfo is not available on your system, unable to detect driver version")

    for card in drivers.get_gpus():
        # pylint: disable=logging-format-interpolation
        try:
            logger.info("GPU: {PCI_ID} {PCI_SUBSYS_ID} ({DRIVER} drivers)".format(**drivers.get_gpu_info(card)))
        except KeyError:
            logger.error("Unable to get GPU information from '%s'", card)

    if drivers.is_outdated():
        setting = "hide-outdated-nvidia-driver-warning"
        if settings.read_setting(setting) != "True":
            DontShowAgainDialog(
                setting,
                _("Your NVIDIA driver is outdated."),
                secondary_message=_(
                    "You are currently running driver %s which does not "
                    "fully support all features for Vulkan and DXVK games.\n"
                    "Please upgrade your driver as described in our "
                    "<a href='%s'>installation guide</a>"
                ) % (
                    driver_info["nvrm"]["version"],
                    settings.DRIVER_HOWTO_URL,
                )
            )

check_libs(all_components=False)

Checks that required libraries are installed on the system

Source code in lutris/startup.py
def check_libs(all_components=False):
    """Checks that required libraries are installed on the system"""
    missing_libs = LINUX_SYSTEM.get_missing_libs()
    if all_components:
        components = LINUX_SYSTEM.requirements
    else:
        components = LINUX_SYSTEM.critical_requirements
    missing_vulkan_libs = []
    for req in components:
        for index, arch in enumerate(LINUX_SYSTEM.runtime_architectures):
            for lib in missing_libs[req][index]:
                if req == "VULKAN":
                    missing_vulkan_libs.append(arch)
                logger.error("%s %s missing (needed by %s)", arch, lib, req.lower())

    if missing_vulkan_libs:
        setting = "dismiss-missing-vulkan-library-warning"
        if settings.read_setting(setting) != "True":
            DontShowAgainDialog(
                setting,
                _("Missing vulkan libraries"),
                secondary_message=_(
                    "Lutris was unable to detect Vulkan support for "
                    "the %s architecture.\n"
                    "This will prevent many games and programs from working.\n"
                    "To install it, please use the following guide: "
                    "<a href='%s'>Installing Graphics Drivers</a>"
                ) % (
                    _(" and ").join(missing_vulkan_libs),
                    settings.DRIVER_HOWTO_URL,
                )
            )

check_vulkan()

Reports if Vulkan is enabled on the system

Source code in lutris/startup.py
def check_vulkan():
    """Reports if Vulkan is enabled on the system"""
    if not vkquery.is_vulkan_supported():
        logger.warning("Vulkan is not available or your system isn't Vulkan capable")

fill_missing_platforms()

Sets the platform on games where it's missing. This should never happen.

Source code in lutris/startup.py
def fill_missing_platforms():
    """Sets the platform on games where it's missing.
    This should never happen.
    """
    pga_games = get_games(filters={"installed": 1})
    for pga_game in pga_games:
        if pga_game.get("platform") or not pga_game["runner"]:
            continue
        game = Game(game_id=pga_game["id"])
        game.set_platform_from_runner()
        if game.platform:
            logger.info("Platform for %s set to %s", game.name, game.platform)
            game.save(save_config=False)

init_dirs()

Creates Lutris directories

Source code in lutris/startup.py
def init_dirs():
    """Creates Lutris directories"""
    directories = [
        settings.CONFIG_DIR,
        os.path.join(settings.CONFIG_DIR, "runners"),
        os.path.join(settings.CONFIG_DIR, "games"),
        settings.DATA_DIR,
        os.path.join(settings.DATA_DIR, "covers"),
        settings.ICON_PATH,
        os.path.join(settings.CACHE_DIR, "banners"),
        os.path.join(settings.CACHE_DIR, "coverart"),
        os.path.join(settings.DATA_DIR, "runners"),
        os.path.join(settings.DATA_DIR, "lib"),
        settings.RUNTIME_DIR,
        settings.CACHE_DIR,
        settings.SHADER_CACHE_DIR,
        os.path.join(settings.CACHE_DIR, "installer"),
        os.path.join(settings.CACHE_DIR, "tmp"),
    ]
    for directory in directories:
        create_folder(directory)

init_lutris()

Run full initialization of Lutris

Source code in lutris/startup.py
def init_lutris():
    """Run full initialization of Lutris"""
    logger.info("Starting Lutris %s", settings.VERSION)
    runners.inject_runners(load_json_runners())
    # Load runner names and platforms
    runners.RUNNER_NAMES = runners.get_runner_names()
    runners.RUNNER_PLATFORMS = runners.get_platforms()
    init_dirs()
    try:
        syncdb()
    except sqlite3.DatabaseError as err:
        raise RuntimeError(
            "Failed to open database file in %s. Try renaming this file and relaunch Lutris" %
            settings.PGA_DB
        ) from err
    for service in DEFAULT_SERVICES:
        if not settings.read_setting(service, section="services"):
            settings.write_setting(service, True, section="services")

run_all_checks()

Run all startup checks

Source code in lutris/startup.py
def run_all_checks():
    """Run all startup checks"""
    check_driver()
    check_libs()
    check_vulkan()
    fill_missing_platforms()

update_runtime(force=False)

Update runtime components

Source code in lutris/startup.py
def update_runtime(force=False):
    """Update runtime components"""
    runtime_call = update_cache.get_last_call("runtime")
    if force or not runtime_call or runtime_call > 3600 * 12:
        runtime_updater = RuntimeUpdater()
        components_to_update = runtime_updater.update()
        if components_to_update:
            while runtime_updater.current_updates:
                time.sleep(0.3)
        update_cache.write_date_to_cache("runtime")
    for dll_manager_class in (DXVKManager, DXVKNVAPIManager, VKD3DManager, D3DExtrasManager, dgvoodoo2Manager):
        key = dll_manager_class.__name__
        key_call = update_cache.get_last_call(key)
        if force or not key_call or key_call > 3600 * 6:
            dll_manager = dll_manager_class()
            dll_manager.upgrade()
            update_cache.write_date_to_cache(key)
    media_call = update_cache.get_last_call("media")
    if force or not media_call or media_call > 3600 * 24:
        sync_media()
        update_cache.write_date_to_cache("media")
    logger.info("Startup complete")

style_manager

PORTAL_BUS_NAME

PORTAL_OBJECT_PATH

PORTAL_SETTINGS_INTERFACE

ColorScheme (Enum)

An enumeration.

Source code in lutris/style_manager.py
class ColorScheme(enum.Enum):

    NO_PREFERENCE = 0  # Default
    PREFER_DARK = 1
    PREFER_LIGHT = 2

NO_PREFERENCE

PREFER_DARK

PREFER_LIGHT

StyleManager (Object)

Manages the color scheme of the app.

Has a single readable GObject property is_dark telling whether the app is in dark mode, it is set to True, when either the user preference on the preferences panel or in the a system is set to prefer dark mode.

Source code in lutris/style_manager.py
class StyleManager(GObject.Object):
    """Manages the color scheme of the app.

    Has a single readable GObject property `is_dark` telling whether the app is
    in dark mode, it is set to True, when either the user preference on the
    preferences panel or in the a system is set to prefer dark mode.
    """

    _color_scheme = ColorScheme.NO_PREFERENCE
    _dbus_proxy = None
    _is_config_dark = False
    _is_dark = False
    _is_system_dark = False

    def __init__(self):
        super().__init__()

        self.gtksettings = Gtk.Settings.get_default()
        self.is_config_dark = settings.read_setting("dark_theme", default="false").lower() == "true"

        Gio.DBusProxy.new_for_bus(
            Gio.BusType.SESSION,
            Gio.DBusProxyFlags.NONE,
            None,
            PORTAL_BUS_NAME,
            PORTAL_OBJECT_PATH,
            PORTAL_SETTINGS_INTERFACE,
            None,
            self._new_for_bus_cb,
        )

    def _read_portal_setting(self) -> None:
        if not self._dbus_proxy:
            return

        variant = GLib.Variant.new_tuple(
            GLib.Variant.new_string("org.freedesktop.appearance"),
            GLib.Variant.new_string("color-scheme"),
        )
        self._dbus_proxy.call(
            "Read",
            variant,
            Gio.DBusCallFlags.NONE,
            GObject.G_MAXINT,
            None,
            self._call_cb,
        )

    def _new_for_bus_cb(self, obj, result):
        try:
            proxy = obj.new_for_bus_finish(result)
            if proxy:
                proxy.connect("g-signal", self._on_settings_changed)
                self._dbus_proxy = proxy
                self._read_portal_setting()
            else:
                raise Exception("Could not start GDBusProxy")

        except Exception as err:
            logger.error("Unable to start Settings portal: %s", err)

    def _call_cb(self, obj, result):
        try:
            values = obj.call_finish(result)
            if values:
                value = values[0]
                self.color_scheme = self._read_value(value)
            else:
                raise Exception("Could not read color-scheme")
        except Exception:
            pass

    def _on_settings_changed(self, _proxy, _sender_name, signal_name, params):
        if signal_name != "SettingChanged":
            return

        namespace, name, value = params

        if namespace == "org.freedesktop.appearance" and name == "color-scheme":
            self.color_scheme = self._read_value(value)

    def _read_value(self, value: int) -> ColorScheme:
        if value == 1:
            return ColorScheme.PREFER_DARK

        if value == 2:
            return ColorScheme.PREFER_LIGHT

        return ColorScheme.NO_PREFERENCE

    @property
    def is_system_dark(self) -> bool:
        return self._is_system_dark

    @is_system_dark.setter  # type: ignore
    def is_system_dark(self, is_system_dark: bool) -> None:
        if self._is_system_dark == is_system_dark:
            return

        self._is_system_dark = is_system_dark
        self._set_is_dark(self._is_config_dark or is_system_dark)

    @property
    def is_config_dark(self) -> bool:
        return self._is_config_dark

    @is_config_dark.setter  # type: ignore
    def is_config_dark(self, is_config_dark: bool) -> None:
        if self._is_config_dark == is_config_dark:
            return

        self._is_config_dark = is_config_dark
        self._set_is_dark(is_config_dark or self._is_system_dark)

    @GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READABLE)
    def is_dark(self) -> bool:
        return self._is_dark

    def _set_is_dark(self, is_dark: bool) -> None:
        if self._is_dark == is_dark:
            return

        self._is_dark = is_dark
        self.notify("is-dark")

        self.gtksettings.set_property(
            "gtk-application-prefer-dark-theme", is_dark
        )

    @property
    def color_scheme(self) -> ColorScheme:
        return self._color_scheme

    @color_scheme.setter  # type: ignore
    def color_scheme(self, color_scheme: ColorScheme) -> None:
        if self._color_scheme == color_scheme:
            return

        self._color_scheme = color_scheme

        self.is_system_dark = self.color_scheme == ColorScheme.PREFER_DARK

color_scheme: ColorScheme property writable

is_config_dark: bool property writable

is_system_dark: bool property writable

__init__(self) special

Source code in lutris/style_manager.py
def __init__(self):
    super().__init__()

    self.gtksettings = Gtk.Settings.get_default()
    self.is_config_dark = settings.read_setting("dark_theme", default="false").lower() == "true"

    Gio.DBusProxy.new_for_bus(
        Gio.BusType.SESSION,
        Gio.DBusProxyFlags.NONE,
        None,
        PORTAL_BUS_NAME,
        PORTAL_OBJECT_PATH,
        PORTAL_SETTINGS_INTERFACE,
        None,
        self._new_for_bus_cb,
    )

do_get_property(self, pspec)

Source code in lutris/style_manager.py
def obj_get_property(self, pspec):
    name = pspec.name.replace('-', '_')
    return getattr(self, name, None)

do_set_property(self, pspec, value)

Source code in lutris/style_manager.py
def obj_set_property(self, pspec, value):
    name = pspec.name.replace('-', '_')
    prop = getattr(cls, name, None)
    if prop:
        prop.fset(self, value)

sysoptions

Options list for system config.

VULKAN_DATA_DIRS

system_options

get_gpu_vendor_cmd(nvidia_files)

Run glxinfo command to get vendor based on certain conditions

Source code in lutris/sysoptions.py
def get_gpu_vendor_cmd(nvidia_files):
    """Run glxinfo command to get vendor based on certain conditions"""
    glxinfocmd = "glxinfo | grep -i opengl | grep -i vendor"

    if USE_DRI_PRIME == 1:
        glxinfocmd = "DRI_PRIME=1 glxinfo | grep -i opengl | grep -i vendor"
    elif nvidia_files == 1:
        glxinfocmd = "__GLX_VENDOR_LIBRARY_NAME=nvidia glxinfo | grep -i opengl | grep -i vendor"
    return glxinfocmd

get_optirun_choices()

Return menu choices (label, value) for Optimus

Source code in lutris/sysoptions.py
def get_optirun_choices():
    """Return menu choices (label, value) for Optimus"""
    choices = [(_("Off"), "off")]
    if system.find_executable("primusrun"):
        choices.append(("primusrun", "primusrun"))
    if system.find_executable("optirun"):
        choices.append(("optirun/virtualgl", "optirun"))
    if system.find_executable("pvkrun"):
        choices.append(("primus vk", "pvkrun"))
    return choices

get_output_choices()

Return list of outputs for drop-downs

Source code in lutris/sysoptions.py
def get_output_choices():
    """Return list of outputs for drop-downs"""
    displays = DISPLAY_MANAGER.get_display_names()
    output_choices = list(zip(displays, displays))
    output_choices.insert(0, (_("Off"), "off"))
    output_choices.insert(1, (_("Primary"), "primary"))
    return output_choices

get_output_list()

Return a list of output with their index. This is used to indicate to SDL 1.2 which monitor to use.

Source code in lutris/sysoptions.py
def get_output_list():
    """Return a list of output with their index.
    This is used to indicate to SDL 1.2 which monitor to use.
    """
    choices = [(_("Off"), "off")]
    displays = DISPLAY_MANAGER.get_display_names()
    for index, output in enumerate(displays):
        # Display name can't be used because they might not be in the right order
        # Using DISPLAYS to get the number of connected monitors
        choices.append((output, str(index)))
    return choices

get_resolution_choices()

Return list of available resolutions as label, value tuples suitable for inclusion in drop-downs.

Source code in lutris/sysoptions.py
def get_resolution_choices():
    """Return list of available resolutions as label, value tuples
    suitable for inclusion in drop-downs.
    """
    resolutions = DISPLAY_MANAGER.get_resolutions()
    resolution_choices = list(zip(resolutions, resolutions))
    resolution_choices.insert(0, (_("Keep current"), "off"))
    return resolution_choices

get_vk_icd_choices()

Return available Vulkan ICD loaders

Source code in lutris/sysoptions.py
def get_vk_icd_choices():
    """Return available Vulkan ICD loaders"""
    intel = []
    amdradv = []
    nvidia = []
    amdvlk = []
    choices = [(_("Auto: WARNING -- No Vulkan Loader detected!"), "")]
    icd_files = defaultdict(list)
    # Add loaders
    for data_dir in VULKAN_DATA_DIRS:
        path = os.path.join(data_dir, "icd.d", "*.json")
        for loader in glob.glob(path):
            icd_key = os.path.basename(loader).split(".")[0]
            icd_files[icd_key].append(os.path.join(path, loader))
            if "intel" in loader:
                intel.append(loader)
            elif "radeon" in loader:
                amdradv.append(loader)
            elif "nvidia" in loader:
                nvidia.append(loader)
            elif "amd_icd" in loader:
                amdvlk.append(loader)

    intel_files = ":".join(intel)
    amdradv_files = ":".join(amdradv)
    nvidia_files = ":".join(nvidia)
    amdvlk_files = ":".join(amdvlk)

    glxinfocmd = get_gpu_vendor_cmd(0)
    if nvidia_files:
        glxinfocmd = get_gpu_vendor_cmd(1)
    with subprocess.Popen(glxinfocmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as glxvendorget:
        glxvendor = glxvendorget.communicate()[0].decode("utf-8")
    default_gpu = glxvendor

    if "Intel" in default_gpu:
        choices = [(_("Auto: Intel Open Source (MESA: ANV)"), intel_files)]
    elif "AMD" in default_gpu:
        choices = [(_("Auto: AMD RADV Open Source (MESA: RADV)"), amdradv_files)]
    elif "NVIDIA" in default_gpu:
        choices = [(_("Auto: Nvidia Proprietary"), nvidia_files)]

    if intel_files:
        choices.append(("Intel Open Source (MESA: ANV)", intel_files))
    if amdradv_files:
        choices.append(("AMD RADV Open Source (MESA: RADV)", amdradv_files))
    if nvidia_files:
        choices.append(("Nvidia Proprietary", nvidia_files))
    if amdvlk_files:
        choices.append(("AMDVLK/AMDGPU-PRO Proprietary", amdvlk_files))
    return choices

with_runner_overrides(runner_slug)

Return system options updated with overrides from given runner.

Source code in lutris/sysoptions.py
def with_runner_overrides(runner_slug):
    """Return system options updated with overrides from given runner."""
    options = system_options
    try:
        runner = runners.import_runner(runner_slug)
    except runners.InvalidRunner:
        return options
    if not getattr(runner, "system_options_override"):
        runner = runner()
    if runner.system_options_override:
        opts_dict = OrderedDict((opt["option"], opt) for opt in options)
        for option in runner.system_options_override:
            key = option["option"]
            if opts_dict.get(key):
                opts_dict[key] = opts_dict[key].copy()
                opts_dict[key].update(option)
            else:
                opts_dict[key] = option
        options = list(opts_dict.values())
    return options

util special

Misc common functions

selective_merge(base_obj, delta_obj)

used by write_json

Source code in lutris/util/__init__.py
def selective_merge(base_obj, delta_obj):
    """ used by write_json """
    if not isinstance(base_obj, dict):
        return delta_obj
    common_keys = set(base_obj).intersection(delta_obj)
    new_keys = set(delta_obj).difference(common_keys)
    for k in common_keys:
        base_obj[k] = selective_merge(base_obj[k], delta_obj[k])
    for k in new_keys:
        base_obj[k] = delta_obj[k]
    return base_obj

audio

Whatever it is we want to do with audio module

reset_pulse()

Reset pulseaudio.

Source code in lutris/util/audio.py
def reset_pulse():
    """Reset pulseaudio."""
    if not system.find_executable("pulseaudio"):
        logger.warning("PulseAudio not installed. Nothing to do.")
        return
    system.execute(["pulseaudio", "--kill"])
    time.sleep(1)
    system.execute(["pulseaudio", "--start"])
    logger.debug("PulseAudio restarted")

cookies

WebkitCookieJar (MozillaCookieJar)

Subclass of MozillaCookieJar for compatibility with cookies coming from Webkit2. This disables the magic_re header which is not present and adds compatibility with HttpOnly cookies (See http://bugs.python.org/issue2190)

Source code in lutris/util/cookies.py
class WebkitCookieJar(MozillaCookieJar):

    """Subclass of MozillaCookieJar for compatibility with cookies
    coming from Webkit2.
    This disables the magic_re header which is not present and adds
    compatibility with HttpOnly cookies (See http://bugs.python.org/issue2190)
    """

    def _really_load(self, f, filename, ignore_discard, ignore_expires):  # pylint: disable=too-many-locals
        now = time.time()
        try:
            while 1:
                line = f.readline()
                if line == "":
                    break

                # last field may be absent, so keep any trailing tab
                if line.endswith("\n"):
                    line = line[:-1]

                sline = line.strip()
                # support HttpOnly cookies (as stored by curl or old Firefox).
                if sline.startswith("#HttpOnly_"):
                    line = sline[10:]
                elif sline.startswith("#") or sline == "":
                    continue

                domain, domain_specified, path, secure, expires, name, value, *_extra = line.split("\t")
                secure = secure == "TRUE"
                domain_specified = domain_specified == "TRUE"
                if name == "":
                    # cookies.txt regards 'Set-Cookie: foo' as a cookie
                    # with no name, whereas http.cookiejar regards it as a
                    # cookie with no value.
                    name = value
                    value = None

                initial_dot = domain.startswith(".")
                assert domain_specified == initial_dot

                discard = False
                if expires == "":
                    expires = None
                    discard = True

                # assume path_specified is false
                c = Cookie(
                    0,
                    name,
                    value,
                    None,
                    False,
                    domain,
                    domain_specified,
                    initial_dot,
                    path,
                    False,
                    secure,
                    expires,
                    discard,
                    None,
                    None,
                    {},
                )
                if not ignore_discard and c.discard:
                    continue
                if not ignore_expires and c.is_expired(now):
                    continue
                self.set_cookie(c)

        except OSError:
            raise
        except Exception as err:
            _warn_unhandled_exception()
            raise OSError("invalid Netscape format cookies file %r: %r" % (filename, line)) from err

datapath

Utility to get the path of Lutris assets

get()

Return the path for the resources.

Source code in lutris/util/datapath.py
def get():
    """Return the path for the resources."""
    launch_path = os.path.realpath(sys.path[0])
    if launch_path.startswith("/usr/local"):
        data_path = "/usr/local/share/lutris"
    elif launch_path.startswith("/usr"):
        data_path = "/usr/share/lutris"
    elif system.path_exists(os.path.normpath(os.path.join(sys.path[0], "share"))):
        data_path = os.path.normpath(os.path.join(sys.path[0], "share/lutris"))
    elif system.path_exists(os.path.normpath(os.path.join(launch_path, "../../share/lutris"))):
        data_path = os.path.normpath(os.path.join(launch_path, "../../share/lutris"))
    else:
        import lutris

        lutris_module = lutris.__file__
        data_path = os.path.join(os.path.dirname(os.path.dirname(lutris_module)), "share/lutris")
    if not system.path_exists(data_path):
        raise IOError("data_path can't be found at : %s" % data_path)
    return data_path

display

Module to deal with various aspects of displays

DBUS_AVAILABLE

DISPLAY_MANAGER

GnomeDesktop

LIB_GNOME_DESKTOP_AVAILABLE

SCREEN_SAVER_INHIBITOR

USE_DRI_PRIME

DBusScreenSaverInhibitor

Inhibit and uninhibit the screen saver using DBus. Requires the Inhibit() and UnInhibit() methods to be exposed over DBus.

Source code in lutris/util/display.py
class DBusScreenSaverInhibitor:

    """Inhibit and uninhibit the screen saver using DBus.
    Requires the Inhibit() and UnInhibit() methods to be exposed over DBus."""

    def __init__(self, name, path, interface, bus_type=Gio.BusType.SESSION):
        self.proxy = Gio.DBusProxy.new_for_bus_sync(
            bus_type, Gio.DBusProxyFlags.NONE, None, name, path, interface, None)

    def inhibit(self, game_name):
        """Inhibit the screen saver.
        Returns a cookie that must be passed to the corresponding uninhibit() call.
        If an error occurs, None is returned instead."""
        try:
            return self.proxy.Inhibit("(ss)", "Lutris", "Running game: %s" % game_name)
        except Exception:
            return None

    def uninhibit(self, cookie):
        """Uninhibit the screen saver.
        Takes a cookie as returned by inhibit. If cookie is None, no action is taken."""
        if cookie is not None:
            self.proxy.UnInhibit("(u)", cookie)
__init__(self, name, path, interface, bus_type=<enum G_BUS_TYPE_SESSION of type Gio.BusType>) special
Source code in lutris/util/display.py
def __init__(self, name, path, interface, bus_type=Gio.BusType.SESSION):
    self.proxy = Gio.DBusProxy.new_for_bus_sync(
        bus_type, Gio.DBusProxyFlags.NONE, None, name, path, interface, None)
inhibit(self, game_name)

Inhibit the screen saver. Returns a cookie that must be passed to the corresponding uninhibit() call. If an error occurs, None is returned instead.

Source code in lutris/util/display.py
def inhibit(self, game_name):
    """Inhibit the screen saver.
    Returns a cookie that must be passed to the corresponding uninhibit() call.
    If an error occurs, None is returned instead."""
    try:
        return self.proxy.Inhibit("(ss)", "Lutris", "Running game: %s" % game_name)
    except Exception:
        return None
uninhibit(self, cookie)

Uninhibit the screen saver. Takes a cookie as returned by inhibit. If cookie is None, no action is taken.

Source code in lutris/util/display.py
def uninhibit(self, cookie):
    """Uninhibit the screen saver.
    Takes a cookie as returned by inhibit. If cookie is None, no action is taken."""
    if cookie is not None:
        self.proxy.UnInhibit("(u)", cookie)

DesktopEnvironment (Enum)

Enum of desktop environments.

Source code in lutris/util/display.py
class DesktopEnvironment(enum.Enum):

    """Enum of desktop environments."""

    PLASMA = 0
    MATE = 1
    XFCE = 2
    DEEPIN = 3
    UNKNOWN = 999
DEEPIN
MATE
PLASMA
UNKNOWN
XFCE

DisplayManager

Get display and resolution using GnomeDesktop

Source code in lutris/util/display.py
class DisplayManager:
    """Get display and resolution using GnomeDesktop"""

    def __init__(self):
        if not LIB_GNOME_DESKTOP_AVAILABLE:
            logger.warning("libgnomedesktop unavailable")
            return
        screen = Gdk.Screen.get_default()
        if not screen:
            raise NoScreenDetected
        self.rr_screen = GnomeDesktop.RRScreen.new(screen)
        self.rr_config = GnomeDesktop.RRConfig.new_current(self.rr_screen)
        self.rr_config.load_current()

    def get_display_names(self):
        """Return names of connected displays"""
        return [output_info.get_display_name() for output_info in self.rr_config.get_outputs()]

    def get_resolutions(self):
        """Return available resolutions"""
        resolutions = ["%sx%s" % (mode.get_width(), mode.get_height()) for mode in self.rr_screen.list_modes()]
        return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)

    def _get_primary_output(self):
        """Return the RROutput used as a primary display"""
        for output in self.rr_screen.list_outputs():
            if output.get_is_primary():
                return output
        return

    def get_current_resolution(self):
        """Return the current resolution for the primary display"""
        output = self._get_primary_output()
        if not output:
            logger.error("Failed to get a default output")
            return "", ""
        current_mode = output.get_current_mode()
        return str(current_mode.get_width()), str(current_mode.get_height())

    @staticmethod
    def set_resolution(resolution):
        """Set the resolution of one or more displays.
        The resolution can either be a string, which will be applied to the
        primary display or a list of configurations as returned by `get_config`.
        This method uses XrandR and will not work on Wayland.
        """
        return change_resolution(resolution)

    @staticmethod
    def get_config():
        """Return the current display resolution
        This method uses XrandR and will not work on wayland
        The output can be fed in `set_resolution`
        """
        return get_outputs()
__init__(self) special
Source code in lutris/util/display.py
def __init__(self):
    if not LIB_GNOME_DESKTOP_AVAILABLE:
        logger.warning("libgnomedesktop unavailable")
        return
    screen = Gdk.Screen.get_default()
    if not screen:
        raise NoScreenDetected
    self.rr_screen = GnomeDesktop.RRScreen.new(screen)
    self.rr_config = GnomeDesktop.RRConfig.new_current(self.rr_screen)
    self.rr_config.load_current()
get_config() staticmethod

Return the current display resolution This method uses XrandR and will not work on wayland The output can be fed in set_resolution

Source code in lutris/util/display.py
@staticmethod
def get_config():
    """Return the current display resolution
    This method uses XrandR and will not work on wayland
    The output can be fed in `set_resolution`
    """
    return get_outputs()
get_current_resolution(self)

Return the current resolution for the primary display

Source code in lutris/util/display.py
def get_current_resolution(self):
    """Return the current resolution for the primary display"""
    output = self._get_primary_output()
    if not output:
        logger.error("Failed to get a default output")
        return "", ""
    current_mode = output.get_current_mode()
    return str(current_mode.get_width()), str(current_mode.get_height())
get_display_names(self)

Return names of connected displays

Source code in lutris/util/display.py
def get_display_names(self):
    """Return names of connected displays"""
    return [output_info.get_display_name() for output_info in self.rr_config.get_outputs()]
get_resolutions(self)

Return available resolutions

Source code in lutris/util/display.py
def get_resolutions(self):
    """Return available resolutions"""
    resolutions = ["%sx%s" % (mode.get_width(), mode.get_height()) for mode in self.rr_screen.list_modes()]
    return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
set_resolution(resolution) staticmethod

Set the resolution of one or more displays. The resolution can either be a string, which will be applied to the primary display or a list of configurations as returned by get_config. This method uses XrandR and will not work on Wayland.

Source code in lutris/util/display.py
@staticmethod
def set_resolution(resolution):
    """Set the resolution of one or more displays.
    The resolution can either be a string, which will be applied to the
    primary display or a list of configurations as returned by `get_config`.
    This method uses XrandR and will not work on Wayland.
    """
    return change_resolution(resolution)

NoScreenDetected (Exception)

Raise this when unable to detect screens

Source code in lutris/util/display.py
class NoScreenDetected(Exception):

    """Raise this when unable to detect screens"""

disable_compositing()

Disable compositing if not already disabled.

Source code in lutris/util/display.py
def disable_compositing():
    """Disable compositing if not already disabled."""
    compositing_enabled = is_compositing_enabled()
    if compositing_enabled is None:
        compositing_enabled = True
    if any(_COMPOSITING_DISABLED_STACK):
        compositing_enabled = False
    _COMPOSITING_DISABLED_STACK.append(compositing_enabled)
    if not compositing_enabled:
        return
    _, stop_compositor = _get_compositor_commands()
    if stop_compositor:
        _run_command(*stop_compositor)

enable_compositing()

Re-enable compositing if the corresponding call to disable_compositing disabled it.

Source code in lutris/util/display.py
def enable_compositing():
    """Re-enable compositing if the corresponding call to disable_compositing
    disabled it."""

    compositing_disabled = _COMPOSITING_DISABLED_STACK.pop()
    if not compositing_disabled:
        return
    start_compositor, _ = _get_compositor_commands()
    if start_compositor:
        _run_command(*start_compositor)

get_default_dpi()

Computes the DPI to use for the primary monitor which we pass to WINE.

Source code in lutris/util/display.py
def get_default_dpi():
    """Computes the DPI to use for the primary monitor
    which we pass to WINE."""
    display = Gdk.Display.get_default()
    monitor = display.get_primary_monitor()
    scale = monitor.get_scale_factor()
    dpi = 96 * scale
    return int(dpi)

get_desktop_environment()

Converts the value of the DESKTOP_SESSION environment variable to one of the constants in the DesktopEnvironment class. Returns None if DESKTOP_SESSION is empty or unset.

Source code in lutris/util/display.py
def get_desktop_environment():
    """Converts the value of the DESKTOP_SESSION environment variable
    to one of the constants in the DesktopEnvironment class.
    Returns None if DESKTOP_SESSION is empty or unset.
    """
    desktop_session = os.environ.get("DESKTOP_SESSION", "").lower()
    if not desktop_session:
        return None
    if desktop_session.endswith("plasma"):
        return DesktopEnvironment.PLASMA
    if desktop_session.endswith("mate"):
        return DesktopEnvironment.MATE
    if desktop_session.endswith("xfce"):
        return DesktopEnvironment.XFCE
    if desktop_session.endswith("deepin"):
        return DesktopEnvironment.DEEPIN
    return DesktopEnvironment.UNKNOWN

get_display_manager()

Return the appropriate display manager instance. Defaults to Mutter if available. This is the only one to support Wayland.

Source code in lutris/util/display.py
def get_display_manager():
    """Return the appropriate display manager instance.
    Defaults to Mutter if available. This is the only one to support Wayland.
    """
    if DBUS_AVAILABLE:
        try:
            return MutterDisplayManager()
        except DBusException as ex:
            logger.debug("Mutter DBus service not reachable: %s", ex)
        except Exception as ex:  # pylint: disable=broad-except
            logger.exception("Failed to instanciate MutterDisplayConfig. Please report with exception: %s", ex)
    else:
        logger.error("DBus is not available, lutris was not properly installed.")
    if LIB_GNOME_DESKTOP_AVAILABLE:
        try:
            return DisplayManager()
        except (GLib.Error, NoScreenDetected):
            pass
    return LegacyDisplayManager()

is_compositing_enabled()

Checks whether compositing is currently disabled or enabled. Returns True for enabled, False for disabled, and None if unknown.

Source code in lutris/util/display.py
def is_compositing_enabled():
    """Checks whether compositing is currently disabled or enabled.
    Returns True for enabled, False for disabled, and None if unknown.
    """
    desktop_environment = get_desktop_environment()
    if desktop_environment is DesktopEnvironment.PLASMA:
        return _get_command_output(
            "qdbus", "org.kde.KWin", "/Compositor", "org.kde.kwin.Compositing.active"
        ) == b"true\n"
    if desktop_environment is DesktopEnvironment.MATE:
        return _get_command_output("gsettings", "get org.mate.Marco.general", "compositing-manager") == b"true\n"
    if desktop_environment is DesktopEnvironment.XFCE:
        return _get_command_output(
            "xfconf-query", "--channel=xfwm4", "--property=/general/use_compositing"
        ) == b"true\n"
    if desktop_environment is DesktopEnvironment.DEEPIN:
        return _get_command_output(
            "dbus-send", "--session", "--dest=com.deepin.WMSwitcher", "--type=method_call",
            "--print-reply=literal", "/com/deepin/WMSwitcher", "com.deepin.WMSwitcher.CurrentWM"
        ) == b"deepin wm\n"
    return None

restore_gamma()

Restores gamma to a normal level.

Source code in lutris/util/display.py
def restore_gamma():
    """Restores gamma to a normal level."""
    xgamma_path = system.find_executable("xgamma")
    try:
        subprocess.Popen([xgamma_path, "-gamma", "1.0"])  # pylint: disable=consider-using-with
    except (FileNotFoundError, TypeError):
        logger.warning("xgamma is not available on your system")
    except PermissionError:
        logger.warning("you do not have permission to call xgamma")

dolphin special

cache_reader

Reads the Dolphin game database, stored in a binary format

CACHE_REVISION
DOLPHIN_GAME_CACHE_FILE
DolphinCacheReader
Source code in lutris/util/dolphin/cache_reader.py
class DolphinCacheReader:
    header_size = 20
    structure = {
        'valid': 'b',
        'file_path': 's',
        'file_name': 's',
        'file_size': 8,
        'volume_size': 8,
        'volume_size_is_accurate': 1,
        'is_datel_disc': 1,
        'is_nkit': 1,
        'short_names': 'a',
        'long_names': 'a',
        'short_makers': 'a',
        'long_makers': 'a',
        'descriptions': 'a',
        'internal_name': 's',
        'game_id': 's',
        'gametdb_id': 's',
        'title_id': 8,
        'maker_id': 's',
        'region': 4,
        'country': 4,
        'platform': 1,
        'platform_': 3,
        'blob_type': 4,
        'block_size': 8,
        'compression_method': 's',
        'revision': 2,
        'disc_number': 1,
        'apploader_date': 's',
        'custom_name': 's',
        'custom_description': 's',
        'custom_maker': 's',
        'volume_banner': 'i',
        'custom_banner': 'i',
        'default_cover': 'c',
        'custom_cover': 'c',
    }

    def __init__(self):
        self.offset = 0
        with open(DOLPHIN_GAME_CACHE_FILE, "rb") as dolphin_cache_file:
            self.cache_content = dolphin_cache_file.read()
        if get_word_len(self.cache_content[:4]) != CACHE_REVISION:
            raise Exception('Incompatible Dolphin version')

    def get_game(self):
        game = {}
        for key, i in self.structure.items():
            if i == 's':
                game[key] = self.get_string()
            elif i == 'b':
                game[key] = self.get_boolean()
            elif i == 'a':
                game[key] = self.get_array()
            elif i == 'i':
                game[key] = self.get_image()
            elif i == 'c':
                game[key] = self.get_cover()
            else:
                game[key] = self.get_raw(i)
        return game

    def get_games(self):
        self.offset += self.header_size
        games = []
        while self.offset < len(self.cache_content):
            try:
                games.append(self.get_game())
            except Exception as ex:
                logger.error("Failed to read Dolphin database: %s", ex)
        return games

    def get_boolean(self):
        res = bool(get_word_len(self.cache_content[self.offset:self.offset + 1]))
        self.offset += 1
        return res

    def get_array(self):
        array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
        self.offset += 4
        array = {}
        for _i in range(array_len):
            array_key = self.get_raw(4)
            array[array_key] = self.get_string()
        return array

    def get_image(self):
        data_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
        self.offset += 4
        res = self.cache_content[self.offset:self.offset + data_len * 4]  # vector<u32>
        self.offset += data_len * 4
        width = get_word_len(self.cache_content[self.offset:self.offset + 4])
        self.offset += 4
        height = get_word_len(self.cache_content[self.offset:self.offset + 4])
        self.offset += 4
        return (width, height), res

    def get_cover(self):
        array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
        self.offset += 4
        return self.get_raw(array_len)

    def get_raw(self, word_len):
        res = get_hex_string(self.cache_content[self.offset:self.offset + word_len])
        self.offset += word_len
        return res

    def get_string(self):
        word_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
        self.offset += 4
        string = self.cache_content[self.offset:self.offset + word_len]
        self.offset += word_len
        return string.decode('utf8')
header_size
structure
__init__(self) special
Source code in lutris/util/dolphin/cache_reader.py
def __init__(self):
    self.offset = 0
    with open(DOLPHIN_GAME_CACHE_FILE, "rb") as dolphin_cache_file:
        self.cache_content = dolphin_cache_file.read()
    if get_word_len(self.cache_content[:4]) != CACHE_REVISION:
        raise Exception('Incompatible Dolphin version')
get_array(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_array(self):
    array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
    self.offset += 4
    array = {}
    for _i in range(array_len):
        array_key = self.get_raw(4)
        array[array_key] = self.get_string()
    return array
get_boolean(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_boolean(self):
    res = bool(get_word_len(self.cache_content[self.offset:self.offset + 1]))
    self.offset += 1
    return res
get_cover(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_cover(self):
    array_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
    self.offset += 4
    return self.get_raw(array_len)
get_game(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_game(self):
    game = {}
    for key, i in self.structure.items():
        if i == 's':
            game[key] = self.get_string()
        elif i == 'b':
            game[key] = self.get_boolean()
        elif i == 'a':
            game[key] = self.get_array()
        elif i == 'i':
            game[key] = self.get_image()
        elif i == 'c':
            game[key] = self.get_cover()
        else:
            game[key] = self.get_raw(i)
    return game
get_games(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_games(self):
    self.offset += self.header_size
    games = []
    while self.offset < len(self.cache_content):
        try:
            games.append(self.get_game())
        except Exception as ex:
            logger.error("Failed to read Dolphin database: %s", ex)
    return games
get_image(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_image(self):
    data_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
    self.offset += 4
    res = self.cache_content[self.offset:self.offset + data_len * 4]  # vector<u32>
    self.offset += data_len * 4
    width = get_word_len(self.cache_content[self.offset:self.offset + 4])
    self.offset += 4
    height = get_word_len(self.cache_content[self.offset:self.offset + 4])
    self.offset += 4
    return (width, height), res
get_raw(self, word_len)
Source code in lutris/util/dolphin/cache_reader.py
def get_raw(self, word_len):
    res = get_hex_string(self.cache_content[self.offset:self.offset + word_len])
    self.offset += word_len
    return res
get_string(self)
Source code in lutris/util/dolphin/cache_reader.py
def get_string(self):
    word_len = get_word_len(self.cache_content[self.offset:self.offset + 4])
    self.offset += 4
    string = self.cache_content[self.offset:self.offset + word_len]
    self.offset += word_len
    return string.decode('utf8')
get_hex_string(string)

Return the hexadecimal representation of a string

Source code in lutris/util/dolphin/cache_reader.py
def get_hex_string(string):
    """Return the hexadecimal representation of a string"""
    return " ".join("{:02x}".format(c) for c in string)
get_word_len(string)

Return the length of a string as specified in the Dolphin format

Source code in lutris/util/dolphin/cache_reader.py
def get_word_len(string):
    """Return the length of a string as specified in the Dolphin format"""
    return int("0x" + "".join("{:02x}".format(c) for c in string[::-1]), 0)

downloader

get_time

Downloader

Non-blocking downloader.

Do start() then check_progress() at regular intervals. Download is done when check_progress() returns 1.0. Stop with cancel().

Source code in lutris/util/downloader.py
class Downloader:

    """Non-blocking downloader.

    Do start() then check_progress() at regular intervals.
    Download is done when check_progress() returns 1.0.
    Stop with cancel().
    """

    (
        INIT,
        DOWNLOADING,
        CANCELLED,
        ERROR,
        COMPLETED
    ) = list(range(5))

    def __init__(self, url, dest, overwrite=False, referer=None, callback=None):
        self.url = url
        self.dest = dest
        self.overwrite = overwrite
        self.referer = referer
        self.stop_request = None
        self.thread = None
        self.callback = callback

        # Read these after a check_progress()
        self.state = self.INIT
        self.error = None
        self.downloaded_size = 0  # Bytes
        self.full_size = 0  # Bytes
        self.progress_fraction = 0
        self.progress_percentage = 0
        self.speed = 0
        self.average_speed = 0
        self.time_left = "00:00:00"  # Based on average speed
        self.last_size = 0
        self.last_check_time = 0
        self.last_speeds = []
        self.speed_check_time = 0
        self.time_left_check_time = 0
        self.file_pointer = None

    def __str__(self):
        return "downloader for %s" % self.url

    def start(self):
        """Start download job."""
        logger.debug("⬇ %s", self.url)
        self.state = self.DOWNLOADING
        self.last_check_time = get_time()
        if self.overwrite and os.path.isfile(self.dest):
            os.remove(self.dest)
        self.file_pointer = open(self.dest, "wb")  # pylint: disable=consider-using-with
        self.thread = jobs.AsyncCall(self.async_download, self.download_cb)
        self.stop_request = self.thread.stop_request

    def reset(self):
        """Reset the state of the downloader"""
        self.state = self.INIT
        self.error = None
        self.downloaded_size = 0  # Bytes
        self.full_size = 0  # Bytes
        self.progress_fraction = 0
        self.progress_percentage = 0
        self.speed = 0
        self.average_speed = 0
        self.time_left = "00:00:00"  # Based on average speed
        self.last_size = 0
        self.last_check_time = 0
        self.last_speeds = []
        self.speed_check_time = 0
        self.time_left_check_time = 0
        self.file_pointer = None

    def check_progress(self):
        """Append last downloaded chunk to dest file and store stats.

        :return: progress (between 0.0 and 1.0)"""
        if self.state not in [self.CANCELLED, self.ERROR]:
            self.get_stats()
        return self.progress_fraction

    def cancel(self):
        """Request download stop and remove destination file."""
        logger.debug("❌ %s", self.url)
        self.state = self.CANCELLED
        if self.stop_request:
            self.stop_request.set()
        if self.file_pointer:
            self.file_pointer.close()
            self.file_pointer = None
        if os.path.isfile(self.dest):
            os.remove(self.dest)

    def download_cb(self, _result, error):
        if error:
            logger.error("Download failed: %s", error)
            self.state = self.ERROR
            self.error = error
            if self.file_pointer:
                self.file_pointer.close()
                self.file_pointer = None
            return

        if self.state == self.CANCELLED:
            return

        logger.debug("Finished downloading %s", self.url)
        if not self.downloaded_size:
            logger.warning("Downloaded file is empty")

        if not self.full_size:
            self.progress_fraction = 1.0
            self.progress_percentage = 100
        self.state = self.COMPLETED
        self.file_pointer.close()
        self.file_pointer = None
        if self.callback:
            self.callback()

    def async_download(self, stop_request=None):
        headers = requests.utils.default_headers()
        headers["User-Agent"] = "Lutris/%s" % __version__
        if self.referer:
            headers["Referer"] = self.referer
        response = requests.get(self.url, headers=headers, stream=True)
        if response.status_code != 200:
            logger.info("%s returned a %s error", self.url, response.status_code)
        response.raise_for_status()
        self.full_size = int(response.headers.get("Content-Length", "").strip() or 0)
        for chunk in response.iter_content(chunk_size=1024):
            if not self.file_pointer:
                break
            if chunk:
                self.downloaded_size += len(chunk)
                self.file_pointer.write(chunk)

    def get_stats(self):
        """Calculate and store download stats."""
        self.speed, self.average_speed = self.get_speed()
        self.time_left = self.get_average_time_left()
        self.last_check_time = get_time()
        self.last_size = self.downloaded_size

        if self.full_size:
            self.progress_fraction = float(self.downloaded_size) / float(self.full_size)
            self.progress_percentage = self.progress_fraction * 100

    def get_speed(self):
        """Return (speed, average speed) tuple."""
        elapsed_time = get_time() - self.last_check_time
        chunk_size = self.downloaded_size - self.last_size
        speed = chunk_size / elapsed_time or 1
        self.last_speeds.append(speed)

        # Average speed
        if get_time() - self.speed_check_time < 1:  # Minimum delay
            return self.speed, self.average_speed

        while len(self.last_speeds) > 20:
            self.last_speeds.pop(0)

        if len(self.last_speeds) > 7:
            # Skim extreme values
            samples = self.last_speeds[1:-1]
        else:
            samples = self.last_speeds[:]

        average_speed = sum(samples) / len(samples)

        self.speed_check_time = get_time()
        return speed, average_speed

    def get_average_time_left(self):
        """Return average download time left as string."""
        if not self.full_size:
            return "???"

        elapsed_time = get_time() - self.time_left_check_time
        if elapsed_time < 1:  # Minimum delay
            return self.time_left

        average_time_left = (self.full_size - self.downloaded_size) / self.average_speed
        minutes, seconds = divmod(average_time_left, 60)
        hours, minutes = divmod(minutes, 60)
        self.time_left_check_time = get_time()
        return "%d:%02d:%02d" % (hours, minutes, seconds)
CANCELLED
COMPLETED
DOWNLOADING
ERROR
INIT
__init__(self, url, dest, overwrite=False, referer=None, callback=None) special
Source code in lutris/util/downloader.py
def __init__(self, url, dest, overwrite=False, referer=None, callback=None):
    self.url = url
    self.dest = dest
    self.overwrite = overwrite
    self.referer = referer
    self.stop_request = None
    self.thread = None
    self.callback = callback

    # Read these after a check_progress()
    self.state = self.INIT
    self.error = None
    self.downloaded_size = 0  # Bytes
    self.full_size = 0  # Bytes
    self.progress_fraction = 0
    self.progress_percentage = 0
    self.speed = 0
    self.average_speed = 0
    self.time_left = "00:00:00"  # Based on average speed
    self.last_size = 0
    self.last_check_time = 0
    self.last_speeds = []
    self.speed_check_time = 0
    self.time_left_check_time = 0
    self.file_pointer = None
__str__(self) special
Source code in lutris/util/downloader.py
def __str__(self):
    return "downloader for %s" % self.url
async_download(self, stop_request=None)
Source code in lutris/util/downloader.py
def async_download(self, stop_request=None):
    headers = requests.utils.default_headers()
    headers["User-Agent"] = "Lutris/%s" % __version__
    if self.referer:
        headers["Referer"] = self.referer
    response = requests.get(self.url, headers=headers, stream=True)
    if response.status_code != 200:
        logger.info("%s returned a %s error", self.url, response.status_code)
    response.raise_for_status()
    self.full_size = int(response.headers.get("Content-Length", "").strip() or 0)
    for chunk in response.iter_content(chunk_size=1024):
        if not self.file_pointer:
            break
        if chunk:
            self.downloaded_size += len(chunk)
            self.file_pointer.write(chunk)
cancel(self)

Request download stop and remove destination file.

Source code in lutris/util/downloader.py
def cancel(self):
    """Request download stop and remove destination file."""
    logger.debug("❌ %s", self.url)
    self.state = self.CANCELLED
    if self.stop_request:
        self.stop_request.set()
    if self.file_pointer:
        self.file_pointer.close()
        self.file_pointer = None
    if os.path.isfile(self.dest):
        os.remove(self.dest)
check_progress(self)

Append last downloaded chunk to dest file and store stats.

:return: progress (between 0.0 and 1.0)

Source code in lutris/util/downloader.py
def check_progress(self):
    """Append last downloaded chunk to dest file and store stats.

    :return: progress (between 0.0 and 1.0)"""
    if self.state not in [self.CANCELLED, self.ERROR]:
        self.get_stats()
    return self.progress_fraction
download_cb(self, _result, error)
Source code in lutris/util/downloader.py
def download_cb(self, _result, error):
    if error:
        logger.error("Download failed: %s", error)
        self.state = self.ERROR
        self.error = error
        if self.file_pointer:
            self.file_pointer.close()
            self.file_pointer = None
        return

    if self.state == self.CANCELLED:
        return

    logger.debug("Finished downloading %s", self.url)
    if not self.downloaded_size:
        logger.warning("Downloaded file is empty")

    if not self.full_size:
        self.progress_fraction = 1.0
        self.progress_percentage = 100
    self.state = self.COMPLETED
    self.file_pointer.close()
    self.file_pointer = None
    if self.callback:
        self.callback()
get_average_time_left(self)

Return average download time left as string.

Source code in lutris/util/downloader.py
def get_average_time_left(self):
    """Return average download time left as string."""
    if not self.full_size:
        return "???"

    elapsed_time = get_time() - self.time_left_check_time
    if elapsed_time < 1:  # Minimum delay
        return self.time_left

    average_time_left = (self.full_size - self.downloaded_size) / self.average_speed
    minutes, seconds = divmod(average_time_left, 60)
    hours, minutes = divmod(minutes, 60)
    self.time_left_check_time = get_time()
    return "%d:%02d:%02d" % (hours, minutes, seconds)
get_speed(self)

Return (speed, average speed) tuple.

Source code in lutris/util/downloader.py
def get_speed(self):
    """Return (speed, average speed) tuple."""
    elapsed_time = get_time() - self.last_check_time
    chunk_size = self.downloaded_size - self.last_size
    speed = chunk_size / elapsed_time or 1
    self.last_speeds.append(speed)

    # Average speed
    if get_time() - self.speed_check_time < 1:  # Minimum delay
        return self.speed, self.average_speed

    while len(self.last_speeds) > 20:
        self.last_speeds.pop(0)

    if len(self.last_speeds) > 7:
        # Skim extreme values
        samples = self.last_speeds[1:-1]
    else:
        samples = self.last_speeds[:]

    average_speed = sum(samples) / len(samples)

    self.speed_check_time = get_time()
    return speed, average_speed
get_stats(self)

Calculate and store download stats.

Source code in lutris/util/downloader.py
def get_stats(self):
    """Calculate and store download stats."""
    self.speed, self.average_speed = self.get_speed()
    self.time_left = self.get_average_time_left()
    self.last_check_time = get_time()
    self.last_size = self.downloaded_size

    if self.full_size:
        self.progress_fraction = float(self.downloaded_size) / float(self.full_size)
        self.progress_percentage = self.progress_fraction * 100
reset(self)

Reset the state of the downloader

Source code in lutris/util/downloader.py
def reset(self):
    """Reset the state of the downloader"""
    self.state = self.INIT
    self.error = None
    self.downloaded_size = 0  # Bytes
    self.full_size = 0  # Bytes
    self.progress_fraction = 0
    self.progress_percentage = 0
    self.speed = 0
    self.average_speed = 0
    self.time_left = "00:00:00"  # Based on average speed
    self.last_size = 0
    self.last_check_time = 0
    self.last_speeds = []
    self.speed_check_time = 0
    self.time_left_check_time = 0
    self.file_pointer = None
start(self)

Start download job.

Source code in lutris/util/downloader.py
def start(self):
    """Start download job."""
    logger.debug("⬇ %s", self.url)
    self.state = self.DOWNLOADING
    self.last_check_time = get_time()
    if self.overwrite and os.path.isfile(self.dest):
        os.remove(self.dest)
    self.file_pointer = open(self.dest, "wb")  # pylint: disable=consider-using-with
    self.thread = jobs.AsyncCall(self.async_download, self.download_cb)
    self.stop_request = self.thread.stop_request

egs special

egs_launcher

Interact with an exiting EGS install

EGSLauncher
Source code in lutris/util/egs/egs_launcher.py
class EGSLauncher:
    manifests_paths = 'ProgramData/Epic/EpicGamesLauncher/Data/Manifests'

    def __init__(self, prefix_path):
        self.prefix_path = prefix_path

    def iter_manifests(self):
        manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
        if not os.path.exists(manifests_path):
            logger.warning("No valid path for EGS games manifests in %s", manifests_path)
            return []
        for manifest in os.listdir(manifests_path):
            if not manifest.endswith(".item"):
                continue
            with open(os.path.join(manifests_path, manifest), encoding='utf-8') as manifest_file:
                manifest_content = json.loads(manifest_file.read())
            if manifest_content["MainGameAppName"] != manifest_content["AppName"]:
                continue
            yield manifest_content
manifests_paths
__init__(self, prefix_path) special
Source code in lutris/util/egs/egs_launcher.py
def __init__(self, prefix_path):
    self.prefix_path = prefix_path
iter_manifests(self)
Source code in lutris/util/egs/egs_launcher.py
def iter_manifests(self):
    manifests_path = os.path.join(self.prefix_path, 'drive_c', self.manifests_paths)
    if not os.path.exists(manifests_path):
        logger.warning("No valid path for EGS games manifests in %s", manifests_path)
        return []
    for manifest in os.listdir(manifests_path):
        if not manifest.endswith(".item"):
            continue
        with open(os.path.join(manifests_path, manifest), encoding='utf-8') as manifest_file:
            manifest_content = json.loads(manifest_file.read())
        if manifest_content["MainGameAppName"] != manifest_content["AppName"]:
            continue
        yield manifest_content

extract

ExtractFailure (Exception)

Exception raised when and archive fails to extract

Source code in lutris/util/extract.py
class ExtractFailure(Exception):
    """Exception raised when and archive fails to extract"""

check_inno_exe(path)

Check if a path in a compatible innosetup archive

Source code in lutris/util/extract.py
def check_inno_exe(path):
    """Check if a path in a compatible innosetup archive"""
    _innoextract_path = get_innoextract_path()
    if not _innoextract_path:
        logger.warning("Innoextract not found, can't determine type of archive %s", path)
        return False
    command = [_innoextract_path, "-i", path]
    return_code = subprocess.call(command)
    return return_code == 0

decompress_gog(file_path, destination_path)

Source code in lutris/util/extract.py
def decompress_gog(file_path, destination_path):
    innoextract_path = get_innoextract_path()
    if not innoextract_path:
        raise OSError("innoextract is not found in the lutris runtime or on the system")
    system.create_folder(destination_path)  # innoextract cannot do mkdir -p
    return_code = subprocess.call([innoextract_path, "-m", "-g", "-d", destination_path, "-e", file_path])
    if return_code != 0:
        raise RuntimeError("innoextract failed to extract GOG setup file")

decompress_gz(file_path, dest_path)

Decompress a gzip file.

Source code in lutris/util/extract.py
def decompress_gz(file_path, dest_path):
    """Decompress a gzip file."""
    if dest_path:
        dest_filename = os.path.join(dest_path, os.path.basename(file_path[:-3]))
    else:
        dest_filename = file_path[:-3]
    os.makedirs(os.path.dirname(dest_filename), exist_ok=True)

    with open(dest_filename, "wb") as dest_file:
        gzipped_file = gzip.open(file_path, "rb")
        dest_file.write(gzipped_file.read())
        gzipped_file.close()
    return dest_path

extract_7zip(path, dest, archive_type=None)

Source code in lutris/util/extract.py
def extract_7zip(path, dest, archive_type=None):
    _7zip_path = os.path.join(settings.RUNTIME_DIR, "p7zip/7z")
    if not system.path_exists(_7zip_path):
        _7zip_path = system.find_executable("7z")
    if not system.path_exists(_7zip_path):
        raise OSError("7zip is not found in the lutris runtime or on the system")
    command = [_7zip_path, "x", path, "-o{}".format(dest), "-aoa"]
    if archive_type and archive_type != "auto":
        command.append("-t{}".format(archive_type))
    subprocess.call(command)

extract_archive(path, to_directory='.', merge_single=True, extractor=None)

Source code in lutris/util/extract.py
def extract_archive(path, to_directory=".", merge_single=True, extractor=None):
    path = os.path.abspath(path)
    logger.debug("Extracting %s to %s", path, to_directory)

    if extractor is None:
        extractor = guess_extractor(path)

    opener, mode = get_archive_opener(extractor)

    temp_path = temp_dir = os.path.join(to_directory, ".extract-%s" % random_id())
    try:
        _do_extract(path, temp_path, opener, mode, extractor)
    except (OSError, zlib.error, tarfile.ReadError, EOFError) as ex:
        logger.error("Extraction failed: %s", ex)
        raise ExtractFailure(str(ex)) from ex
    if merge_single:
        extracted = os.listdir(temp_path)
        if len(extracted) == 1:
            temp_path = os.path.join(temp_path, extracted[0])

    if os.path.isfile(temp_path):
        destination_path = os.path.join(to_directory, extracted[0])
        if os.path.isfile(destination_path):
            logger.warning("Overwrite existing file %s", destination_path)
            os.remove(destination_path)
        if os.path.isdir(destination_path):
            os.rename(destination_path, destination_path + random_id())

        shutil.move(temp_path, to_directory)
        os.removedirs(temp_dir)
    else:
        for archive_file in os.listdir(temp_path):
            source_path = os.path.join(temp_path, archive_file)
            destination_path = os.path.join(to_directory, archive_file)
            # logger.debug("Moving extracted files from %s to %s", source_path, destination_path)

            if system.path_exists(destination_path):
                logger.warning("Overwrite existing path %s", destination_path)
                if os.path.isfile(destination_path):
                    os.remove(destination_path)
                    shutil.move(source_path, destination_path)
                elif os.path.isdir(destination_path):
                    try:
                        system.merge_folders(source_path, destination_path)
                    except OSError as ex:
                        logger.error(
                            "Failed to merge to destination %s: %s",
                            destination_path,
                            ex,
                        )
                        raise ExtractFailure(str(ex)) from ex
            else:
                shutil.move(source_path, destination_path)
        system.remove_folder(temp_dir)
    logger.debug("Finished extracting %s to %s", path, to_directory)
    return path, to_directory

extract_deb(archive, dest)

Extract the contents of a deb file to a destination folder

Source code in lutris/util/extract.py
def extract_deb(archive, dest):
    """Extract the contents of a deb file to a destination folder"""
    extract_7zip(archive, dest, archive_type="ar")
    debian_folder = os.path.join(dest, "debian")
    os.makedirs(debian_folder)

    control_file_exts = [".gz", ".xz", ".zst", ""]
    for extension in control_file_exts:
        control_tar_path = os.path.join(dest, "control.tar{}".format(extension))
        if os.path.exists(control_tar_path):
            shutil.move(control_tar_path, debian_folder)
            break

    data_file_exts = [".gz", ".xz", ".zst", ".bz2", ".lzma", ""]
    for extension in data_file_exts:
        data_tar_path = os.path.join(dest, "data.tar{}".format(extension))
        if os.path.exists(data_tar_path):
            extract_archive(data_tar_path, dest)
            os.remove(data_tar_path)
            break

extract_exe(path, dest)

Source code in lutris/util/extract.py
def extract_exe(path, dest):
    if check_inno_exe(path):
        decompress_gog(path, dest)
    else:
        # use 7za to check if exe is an archive
        _7zip_path = os.path.join(settings.RUNTIME_DIR, "p7zip/7za")
        if not system.path_exists(_7zip_path):
            _7zip_path = system.find_executable("7za")
        if not system.path_exists(_7zip_path):
            raise OSError("7zip is not found in the lutris runtime or on the system")
        command = [_7zip_path, "t", path]
        return_code = subprocess.call(command)
        if return_code == 0:
            extract_7zip(path, dest)
        else:
            raise RuntimeError("specified exe is not an archive or GOG setup file")

extract_gog(path, dest)

Source code in lutris/util/extract.py
def extract_gog(path, dest):
    if check_inno_exe(path):
        decompress_gog(path, dest)
    else:
        raise RuntimeError("specified exe is not a GOG setup file")

get_archive_opener(extractor)

Return the archive opener and optional mode for an extractor

Source code in lutris/util/extract.py
def get_archive_opener(extractor):
    """Return the archive opener and optional mode for an extractor"""
    mode = None
    if extractor == "tar":
        opener, mode = tarfile.open, "r:"
    elif extractor == "tgz":
        opener, mode = tarfile.open, "r:gz"
    elif extractor == "txz":
        opener, mode = tarfile.open, "r:xz"
    elif extractor == "tbz2":
        opener, mode = tarfile.open, "r:bz2"
    elif extractor == "tzst":
        opener, mode = tarfile.open, "r:zst"  # Note: not supported by tarfile yet
    elif extractor == "gzip":
        opener = "gz"
    elif extractor == "gog":
        opener = "innoextract"
    elif extractor == "exe":
        opener = "exe"
    elif extractor == "deb":
        opener = "deb"
    else:
        opener = "7zip"
    return opener, mode

get_innoextract_list(file_path)

Return the list of files contained in a GOG archive

Source code in lutris/util/extract.py
def get_innoextract_list(file_path):
    """Return the list of files contained in a GOG archive"""
    output = system.read_process_output([get_innoextract_path(), "-lmq", file_path])
    return [line[3:] for line in output.split("\n") if line]

get_innoextract_path()

Return the path where innoextract is installed

Source code in lutris/util/extract.py
def get_innoextract_path():
    """Return the path where innoextract is installed"""
    inno_dirs = [path for path in os.listdir(settings.RUNTIME_DIR) if path.startswith("innoextract")]
    if inno_dirs:
        inno_path = os.path.join(settings.RUNTIME_DIR, inno_dirs[0], "innoextract")
    else:
        inno_path = system.find_executable("innoextract")
        if inno_path:
            logger.warning("innoextract not available in the runtime folder, using some random version")
    if system.path_exists(inno_path):
        return inno_path

guess_extractor(path)

Guess what extractor should be used from a file name

Source code in lutris/util/extract.py
def guess_extractor(path):
    """Guess what extractor should be used from a file name"""
    if path.endswith(".tar"):
        extractor = "tar"
    elif path.endswith((".tar.gz", ".tgz")):
        extractor = "tgz"
    elif path.endswith((".tar.xz", ".txz", ".tar.lzma")):
        extractor = "txz"
    elif path.endswith((".tar.bz2", ".tbz2", ".tbz")):
        extractor = "tbz2"
    elif path.endswith((".tar.zst", ".tzst")):
        extractor = "tzst"
    elif path.endswith(".gz"):
        extractor = "gzip"
    elif path.endswith(".exe"):
        extractor = "exe"
    elif path.endswith(".deb"):
        extractor = "deb"
    else:
        extractor = None
    return extractor

is_7zip_supported(path, extractor)

Source code in lutris/util/extract.py
def is_7zip_supported(path, extractor):
    supported_extractors = (
        "7z",
        "xz",
        "bzip2",
        "gzip",
        "tar",
        "zip",
        "ar",
        "arj",
        "cab",
        "chm",
        "cpio",
        "cramfs",
        "dmg",
        "ext",
        "fat",
        "gpt",
        "hfs",
        "ihex",
        "iso",
        "lzh",
        "lzma",
        "mbr",
        "msi",
        "nsis",
        "ntfs",
        "qcow2",
        "rar",
        "rpm",
        "squashfs",
        "udf",
        "uefi",
        "vdi",
        "vhd",
        "vmdk",
        "wim",
        "xar",
        "z",
        "auto",
    )
    if extractor:
        return extractor.lower() in supported_extractors
    _base, ext = os.path.splitext(path)
    if ext:
        ext = ext.lstrip(".").lower()
        return ext in supported_extractors

random_id()

Return a random ID

Source code in lutris/util/extract.py
def random_id():
    """Return a random ID"""
    return str(uuid.uuid4())[:8]

fileio

EvilConfigParser (RawConfigParser)

ConfigParser with support for evil INIs using duplicate keys.

Source code in lutris/util/fileio.py
class EvilConfigParser(RawConfigParser):  # pylint: disable=too-many-ancestors

    """ConfigParser with support for evil INIs using duplicate keys."""

    _SECT_TMPL = r"""
        \[                                 # [
        (?P<header>[^]]+)                  # very permissive!
        \]                                 # ]
        """
    _OPT_TMPL = r"""
        (?P<option>.*?)                    # very permissive!
        \s*(?P<vi>{delim})\s*              # any number of space/tab,
                                           # followed by any of the
                                           # allowed delimiters,
                                           # followed by any space/tab
        (?P<value>.*)$                     # everything up to eol
        """
    _OPT_NV_TMPL = r"""
        (?P<option>.*?)                    # very permissive!
        \s*(?:                             # any number of space/tab,
        (?P<vi>{delim})\s*                 # optionally followed by
                                           # any of the allowed
                                           # delimiters, followed by any
                                           # space/tab
        (?P<value>.*))?$                   # everything up to eol
        """

    # Remove colon from separators since it will mess with some config files
    OPTCRE = re.compile(_OPT_TMPL.format(delim="="), re.VERBOSE)
    OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim="="), re.VERBOSE)

    def write(self, fp, space_around_delimiters=True):
        for section in self._sections:
            fp.write("[{}]\n".format(section).encode("utf-8"))
            for (key, value) in list(self._sections[section].items()):
                if key == "__name__":
                    continue
                if (value is not None) or (self._optcre == self.OPTCRE):
                    # Duplicated keys writing support inside
                    key = "=".join((key, str(value).replace("\n", "\n%s=" % key)))
                fp.write("{}\n".format(key).encode("utf-8"))
            fp.write("\n".encode("utf-8"))
OPTCRE
OPTCRE_NV
write(self, fp, space_around_delimiters=True)

Write an .ini-format representation of the configuration state.

If `space_around_delimiters' is True (the default), delimiters between keys and values are surrounded by spaces.

Please note that comments in the original configuration file are not preserved when writing the configuration back.

Source code in lutris/util/fileio.py
def write(self, fp, space_around_delimiters=True):
    for section in self._sections:
        fp.write("[{}]\n".format(section).encode("utf-8"))
        for (key, value) in list(self._sections[section].items()):
            if key == "__name__":
                continue
            if (value is not None) or (self._optcre == self.OPTCRE):
                # Duplicated keys writing support inside
                key = "=".join((key, str(value).replace("\n", "\n%s=" % key)))
            fp.write("{}\n".format(key).encode("utf-8"))
        fp.write("\n".encode("utf-8"))

MultiOrderedDict (OrderedDict)

dict_type to use with an EvilConfigParser instance.

Source code in lutris/util/fileio.py
class MultiOrderedDict(OrderedDict):

    """dict_type to use with an EvilConfigParser instance."""

    def __setitem__(self, key, value):
        if isinstance(value, list) and key in self:
            self[key].extend(value)
        else:
            super().__setitem__(key, value)
__setitem__(self, key, value) special
Source code in lutris/util/fileio.py
def __setitem__(self, key, value):
    if isinstance(value, list) and key in self:
        self[key].extend(value)
    else:
        super().__setitem__(key, value)

game_finder

Automatically detects game executables in a folder

find_linux_game_executable(path, make_executable=False)

Looks for a binary or shell script that launches the game in a directory

Source code in lutris/util/game_finder.py
def find_linux_game_executable(path, make_executable=False):
    """Looks for a binary or shell script that launches the game in a directory"""

    for base, _dirs, files in os.walk(path):
        candidates = {}
        for _file in files:
            if is_excluded_elf(_file):
                continue
            abspath = os.path.join(base, _file)
            file_type = magic.from_file(abspath)
            if "ASCII text executable" in file_type:
                candidates["shell"] = abspath
            if "Bourne-Again shell script" in file_type:
                candidates["bash"] = abspath
            if "POSIX shell script executable" in file_type:
                candidates["posix"] = abspath
            if "64-bit LSB executable" in file_type:
                candidates["64bit"] = abspath
            if "32-bit LSB executable" in file_type:
                candidates["32bit"] = abspath
        if candidates:
            if make_executable:
                for candidate in candidates.values():
                    system.make_executable(candidate)
            return (
                candidates.get("shell")
                or candidates.get("bash")
                or candidates.get("posix")
                or candidates.get("64bit")
                or candidates.get("32bit")
            )
    logger.error("Couldn't find a Linux executable in %s", path)
    return ""

find_windows_game_executable(path)

Source code in lutris/util/game_finder.py
def find_windows_game_executable(path):
    for base, _dirs, files in os.walk(path):
        candidates = {}
        if is_excluded_dir(base):
            continue
        for _file in files:
            if is_excluded_exe(_file):
                continue
            abspath = os.path.join(base, _file)
            if os.path.islink(abspath):
                continue
            file_type = magic.from_file(abspath)
            if "MS Windows shortcut" in file_type:
                candidates["link"] = abspath
            elif "PE32+ executable (GUI) x86-64" in file_type:
                candidates["64bit"] = abspath
            elif "PE32 executable (GUI) Intel 80386" in file_type:
                candidates["32bit"] = abspath
        if candidates:
            return (
                candidates.get("link")
                or candidates.get("64bit")
                or candidates.get("32bit")
            )
    logger.error("Couldn't find a Windows executable in %s", path)
    return ""

is_excluded_dir(path)

Source code in lutris/util/game_finder.py
def is_excluded_dir(path):
    excluded = (
        "Internet Explorer",
        "Windows NT",
        "Common Files",
        "Windows Media Player",
        "windows",
        "ProgramData",
        "users",
        "GameSpy Arcade"
    )
    return any(dir_name in excluded for dir_name in path.split("/"))

is_excluded_elf(filename)

Source code in lutris/util/game_finder.py
def is_excluded_elf(filename):
    excluded = (
        "xdg-open",
        "uninstall"
    )
    _fn = filename.lower()
    return any(exclude in _fn for exclude in excluded)

is_excluded_exe(filename)

Source code in lutris/util/game_finder.py
def is_excluded_exe(filename):
    excluded = (
        "unins000",
        "uninstal",
        "update",
        "config.exe",
        "gsarcade.exe",
        "dosbox.exe",
    )
    _fn = filename.lower()
    return any(exclude in _fn for exclude in excluded)

gamecontrollerdb

ControllerMapping

Source code in lutris/util/gamecontrollerdb.py
class ControllerMapping:
    valid_keys = [
        "platform",
        "leftx",
        "lefty",
        "rightx",
        "righty",
        "a",
        "b",
        "back",
        "dpdown",
        "dpleft",
        "dpright",
        "dpup",
        "guide",
        "leftshoulder",
        "leftstick",
        "lefttrigger",
        "rightshoulder",
        "rightstick",
        "righttrigger",
        "start",
        "x",
        "y",
    ]

    def __init__(self, guid, name, mapping):
        self.guid = guid
        self.name = name
        self.mapping = mapping
        self.keys = {}
        self.parse()

    def __str__(self):
        return self.name

    def parse(self):
        key_maps = self.mapping.split(",")
        for key_map in key_maps:
            if not key_map:
                continue
            xinput_key, sdl_key = key_map.split(":")
            if xinput_key not in self.valid_keys:
                logger.warning("Unrecognized key %s", xinput_key)
                continue
            self.keys[xinput_key] = sdl_key
valid_keys
__init__(self, guid, name, mapping) special
Source code in lutris/util/gamecontrollerdb.py
def __init__(self, guid, name, mapping):
    self.guid = guid
    self.name = name
    self.mapping = mapping
    self.keys = {}
    self.parse()
__str__(self) special
Source code in lutris/util/gamecontrollerdb.py
def __str__(self):
    return self.name
parse(self)
Source code in lutris/util/gamecontrollerdb.py
def parse(self):
    key_maps = self.mapping.split(",")
    for key_map in key_maps:
        if not key_map:
            continue
        xinput_key, sdl_key = key_map.split(":")
        if xinput_key not in self.valid_keys:
            logger.warning("Unrecognized key %s", xinput_key)
            continue
        self.keys[xinput_key] = sdl_key

GameControllerDB

Source code in lutris/util/gamecontrollerdb.py
class GameControllerDB:
    db_path = os.path.join(RUNTIME_DIR, "gamecontrollerdb/gamecontrollerdb.txt")

    def __init__(self):
        if not system.path_exists(self.db_path):
            raise OSError("Path to gamecontrollerdb.txt not provided or invalid")
        self.controllers = {}
        self.parsedb()

    def __str__(self):
        return "GameControllerDB <%s>" % self.db_path

    def __getitem__(self, value):
        return self.controllers[value]

    def parsedb(self):
        with open(self.db_path, "r", encoding='utf-8') as db:
            for line in db.readlines():
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                guid, name, mapping = line.strip().split(",", 2)
                self.controllers[guid] = ControllerMapping(guid, name, mapping)
db_path
__getitem__(self, value) special
Source code in lutris/util/gamecontrollerdb.py
def __getitem__(self, value):
    return self.controllers[value]
__init__(self) special
Source code in lutris/util/gamecontrollerdb.py
def __init__(self):
    if not system.path_exists(self.db_path):
        raise OSError("Path to gamecontrollerdb.txt not provided or invalid")
    self.controllers = {}
    self.parsedb()
__str__(self) special
Source code in lutris/util/gamecontrollerdb.py
def __str__(self):
    return "GameControllerDB <%s>" % self.db_path
parsedb(self)
Source code in lutris/util/gamecontrollerdb.py
def parsedb(self):
    with open(self.db_path, "r", encoding='utf-8') as db:
        for line in db.readlines():
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            guid, name, mapping = line.strip().split(",", 2)
            self.controllers[guid] = ControllerMapping(guid, name, mapping)

gog

convert_gog_config_to_lutris(gog_config, gog_game_path)

Source code in lutris/util/gog.py
def convert_gog_config_to_lutris(gog_config, gog_game_path):
    play_tasks = gog_config["playTasks"]
    lutris_config = {"launch_configs": []}
    for task in play_tasks:
        config = get_game_config(task, gog_game_path)
        if not config:
            continue
        if task.get("isPrimary"):
            lutris_config.update(config)
        else:
            lutris_config["launch_configs"].append(config)
    return lutris_config

get_game_config(task, gog_game_path)

Source code in lutris/util/gog.py
def get_game_config(task, gog_game_path):
    config = {}
    if "path" not in task:
        return
    exe = task["path"]
    exe_abspath = system.fix_path_case(os.path.join(gog_game_path, exe))
    if os.path.exists(exe_abspath):
        exe = exe_abspath
    else:
        logger.warning("No executable found at %s", exe_abspath)
    config["exe"] = exe
    if task.get("workingDir"):
        config["working_dir"] = task["workingDir"]
    if task.get("arguments"):
        config["args"] = task["arguments"]
    if task.get("name"):
        config["name"] = task["name"]
    return config

get_gog_config(gog_game_path)

Extract runtime information such as executable paths from GOG files

Source code in lutris/util/gog.py
def get_gog_config(gog_game_path):
    """Extract runtime information such as executable paths from GOG files"""
    config_filename = [
        fn
        for fn in os.listdir(gog_game_path)
        if fn.startswith("goggame") and fn.endswith(".info")
    ]
    if not config_filename:
        logger.error("No config file found in %s", gog_game_path)
        return
    gog_config_path = os.path.join(gog_game_path, config_filename[0])
    with open(gog_config_path, encoding='utf-8') as gog_config_file:
        gog_config = json.loads(gog_config_file.read())
    return gog_config

get_gog_config_from_path(target_path)

Return the GOG configuration for a root path

Source code in lutris/util/gog.py
def get_gog_config_from_path(target_path):
    """Return the GOG configuration for a root path"""
    gog_game_path = get_gog_game_path(target_path)
    if gog_game_path:
        return get_gog_config(gog_game_path)

get_gog_game_path(target_path)

Return the absolute path where a GOG game is installed

Source code in lutris/util/gog.py
def get_gog_game_path(target_path):
    """Return the absolute path where a GOG game is installed"""
    gog_game_path = os.path.join(target_path, "drive_c/GOG Games/")
    if not os.path.exists(gog_game_path):
        logger.warning("No 'GOG Games' folder in %s", target_path)
        return None
    games = os.listdir(gog_game_path)
    if len(games) > 1:
        logger.warning("More than 1 game found, this is currently unsupported")
    return os.path.join(gog_game_path, games[0])

graphics special

displayconfig

DBus backed display management for Mutter

CRTC

A CRTC (CRT controller) is a logical monitor, ie a portion of the compositor coordinate space. It might correspond to multiple monitors, when in clone mode, but not that it is possible to implement clone mode also by setting different CRTCs to the same coordinates.

Source code in lutris/util/graphics/displayconfig.py
class CRTC():

    """A CRTC (CRT controller) is a logical monitor, ie a portion of the
    compositor coordinate space. It might correspond to multiple monitors, when
    in clone mode, but not that it is possible to implement clone mode also by
    setting different CRTCs to the same coordinates.
    """

    def __init__(self, crtc_info):
        self.crtc_info = crtc_info

    def __repr__(self):
        return "%s %s %s" % (self.id, self.geometry_str, self.current_mode)

    @property
    def id(self):  # pylint: disable=invalid-name
        """The ID in the API of this CRTC"""
        return str(self.crtc_info[0])

    @property
    def winsys_id(self):
        """the low-level ID of this CRTC
        (which might be a XID, a KMS handle or something entirely different)"""
        return self.crtc_info[1]

    @property
    def geometry_str(self):
        """Return a human readable representation of the geometry"""
        return "%dx%d%s%d%s%d" % (
            self.geometry[0],
            self.geometry[1],
            "" if self.geometry[2] < 0 else "+",
            self.geometry[2],
            "" if self.geometry[3] < 0 else "+",
            self.geometry[3],
        )

    @property
    def geometry(self):
        """The geometry of this CRTC
        (might be invalid if the CRTC is not in use)
        """
        return (int(self.crtc_info[2]), int(self.crtc_info[3]), int(self.crtc_info[4]), int(self.crtc_info[5]))

    @property
    def current_mode(self):
        """The current mode of the CRTC, or -1 if this CRTC is not used
        Note: the size of the mode will always correspond to the width
        and height of the CRTC"""
        return int(self.crtc_info[6])

    @property
    def current_transform(self):
        """The current transform (espressed according to the wayland protocol)"""
        return str(self.crtc_info[7])

    @property
    def transforms(self):
        """All possible transforms"""
        return str(self.crtc_info[8])

    @property
    def properties(self):
        """Other high-level properties that affect this CRTC;
        they are not necessarily reflected in the hardware.
        No property is specified in this version of the API.
        """
        return str(self.crtc_info[9])
current_mode property readonly

The current mode of the CRTC, or -1 if this CRTC is not used Note: the size of the mode will always correspond to the width and height of the CRTC

current_transform property readonly

The current transform (espressed according to the wayland protocol)

geometry property readonly

The geometry of this CRTC (might be invalid if the CRTC is not in use)

geometry_str property readonly

Return a human readable representation of the geometry

id property readonly

The ID in the API of this CRTC

properties property readonly

Other high-level properties that affect this CRTC; they are not necessarily reflected in the hardware. No property is specified in this version of the API.

transforms property readonly

All possible transforms

winsys_id property readonly

the low-level ID of this CRTC (which might be a XID, a KMS handle or something entirely different)

__init__(self, crtc_info) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, crtc_info):
    self.crtc_info = crtc_info
__repr__(self) special
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
    return "%s %s %s" % (self.id, self.geometry_str, self.current_mode)
DisplayConfig (tuple)

DisplayConfig(monitors, name, position, transform, primary, scale)

__getnewargs__(self) special

Return self as a plain tuple. Used by copy and pickle.

Source code in lutris/util/graphics/displayconfig.py
def __getnewargs__(self):
    'Return self as a plain tuple.  Used by copy and pickle.'
    return _tuple(self)
__new__(_cls, monitors, name, position, transform, primary, scale) special staticmethod

Create new instance of DisplayConfig(monitors, name, position, transform, primary, scale)

__repr__(self) special

Return a nicely formatted representation string

Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
    'Return a nicely formatted representation string'
    return self.__class__.__name__ + repr_fmt % self
DisplayMode

Representation of a screen mode (resolution, refresh rate)

Source code in lutris/util/graphics/displayconfig.py
class DisplayMode:

    """Representation of a screen mode (resolution, refresh rate)"""

    def __init__(self, mode_info):
        self.mode_info = mode_info

    def __str__(self):
        return "%sx%s@%s" % (self.width, self.height, self.frequency)

    def __repr__(self):
        return "<DisplayMode: %sx%s@%s>" % (self.width, self.height, self.frequency)

    @property
    def id(self):  # pylint: disable=invalid-name
        """ID of the mode"""
        return str(self.mode_info[0])

    @property
    def winsys_id(self):
        """the low-level ID of this mode"""
        return str(self.mode_info[1])

    @property
    def width(self):
        """width in physical pixels"""
        return int(self.mode_info[2])

    @property
    def height(self):
        """height in physical pixels"""
        return int(self.mode_info[3])

    @property
    def frequency(self):
        """refresh rate"""
        return str(self.mode_info[4])

    @property
    def flags(self):
        """mode flags as defined in xf86drmMode.h and randr.h"""
        return self.mode_info[5]
flags property readonly

mode flags as defined in xf86drmMode.h and randr.h

frequency property readonly

refresh rate

height property readonly

height in physical pixels

id property readonly

ID of the mode

width property readonly

width in physical pixels

winsys_id property readonly

the low-level ID of this mode

__init__(self, mode_info) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, mode_info):
    self.mode_info = mode_info
__repr__(self) special
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
    return "<DisplayMode: %sx%s@%s>" % (self.width, self.height, self.frequency)
__str__(self) special
Source code in lutris/util/graphics/displayconfig.py
def __str__(self):
    return "%sx%s@%s" % (self.width, self.height, self.frequency)
DisplayState

Snapshot of a display configuration at a given time

Source code in lutris/util/graphics/displayconfig.py
class DisplayState:

    """Snapshot of a display configuration at a given time"""

    def __init__(self, interface):
        self.interface = interface
        self._state = self.load_state()

    def load_state(self):
        """Return current state from dbus interface"""
        return self.interface.GetCurrentState()

    @property
    def serial(self):
        """Configuration serial"""
        return self._state[0]

    @property
    def monitors(self):
        """Available monitors"""
        return [Monitor(monitor) for monitor in self._state[1]]

    @property
    def logical_monitors(self):
        """Current logical monitor configuration"""
        return [LogicalMonitor(l_m, self.monitors) for l_m in self._state[2]]

    @property
    def properties(self):
        """Display configuration properties"""
        return self._state[3]

    def get_current_mode(self):
        """Return the current mode"""
        return self.monitors[0].get_current_mode()
logical_monitors property readonly

Current logical monitor configuration

monitors property readonly

Available monitors

properties property readonly

Display configuration properties

serial property readonly

Configuration serial

__init__(self, interface) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, interface):
    self.interface = interface
    self._state = self.load_state()
get_current_mode(self)

Return the current mode

Source code in lutris/util/graphics/displayconfig.py
def get_current_mode(self):
    """Return the current mode"""
    return self.monitors[0].get_current_mode()
load_state(self)

Return current state from dbus interface

Source code in lutris/util/graphics/displayconfig.py
def load_state(self):
    """Return current state from dbus interface"""
    return self.interface.GetCurrentState()
LogicalMonitor

A logical monitor. Similar to CRTCs but logical monitors also contain scaling information.

Source code in lutris/util/graphics/displayconfig.py
class LogicalMonitor:

    """A logical monitor. Similar to CRTCs but logical monitors also contain
    scaling information.
    """

    def __init__(self, lm_info, monitors):
        self._lm = lm_info
        self._monitors = monitors

    @property
    def position(self):
        """Return the position of the monitor"""
        return int(self._lm[0]), int(self._lm[1])

    @property
    def scale(self):
        """Scale"""
        return self._lm[2]

    @property
    def transform(self):
        """Transforms

        Possible transform values:
        0: normal
        1: 90°
        2: 180°
        3: 270°
        4: flipped
        5: 90° flipped
        6: 180° flipped
        7: 270° flipped
        """
        return self._lm[3]

    @property
    def primary(self):
        """True if this is the primary logical monitor"""
        return bool(self._lm[4])

    def _get_monitor_for_connector(self, connector):
        """Return a Monitor instance from its connector name"""
        for monitor in self._monitors:
            if monitor.name == str(connector):
                return monitor
        return

    @property
    def monitors(self):
        """Monitors displaying that logical monitor"""
        return [self._get_monitor_for_connector(m[0]) for m in self._lm[5]]

    @property
    def properties(self):
        """Possibly other properties"""
        return self._lm[6]

    def get_config(self):
        """Export the current configuration so it can be stored then reapplied later"""
        monitors = [(monitor.name, monitor.get_current_mode().id) for monitor in self.monitors]
        return DisplayConfig(monitors, self.monitors[0].name, self.position, self.transform, self.primary, self.scale)
monitors property readonly

Monitors displaying that logical monitor

position property readonly

Return the position of the monitor

primary property readonly

True if this is the primary logical monitor

properties property readonly

Possibly other properties

scale property readonly

Scale

transform property readonly

Transforms

Possible transform values: 0: normal 1: 90° 2: 180° 3: 270° 4: flipped 5: 90° flipped 6: 180° flipped 7: 270° flipped

__init__(self, lm_info, monitors) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, lm_info, monitors):
    self._lm = lm_info
    self._monitors = monitors
get_config(self)

Export the current configuration so it can be stored then reapplied later

Source code in lutris/util/graphics/displayconfig.py
def get_config(self):
    """Export the current configuration so it can be stored then reapplied later"""
    monitors = [(monitor.name, monitor.get_current_mode().id) for monitor in self.monitors]
    return DisplayConfig(monitors, self.monitors[0].name, self.position, self.transform, self.primary, self.scale)
Monitor

A physical monitor

Source code in lutris/util/graphics/displayconfig.py
class Monitor:

    """A physical monitor"""

    def __init__(self, monitor):
        self._monitor = monitor

    def get_current_mode(self):
        """Return the current mode"""
        for mode in self.get_modes():
            if mode.is_current:
                return mode
        return

    def get_modes(self):
        """Return available modes"""
        return [MonitorMode(mode) for mode in self._monitor[1]]

    def get_mode_for_resolution(self, resolution):
        """Return an appropriate mode for a given resolution"""
        width, height = [int(i) for i in resolution.split("x")]
        for mode in self.get_modes():
            if mode.width == width and mode.height == height:
                return mode
        return

    @property
    def name(self):
        """Name of the connector"""
        return str(self._monitor[0][0])

    @property
    def vendor(self):
        """Manufacturer of the monitor"""
        return str(self._monitor[0][1])

    @property
    def model(self):
        """Model name of the monitor"""
        return str(self._monitor[0][2])

    @property
    def serial_number(self):
        """Serial number"""
        return str(self._monitor[0][3])

    @property
    def is_underscanning(self):
        """Return true if the monitor is underscanning"""
        return bool(self._monitor[2]['is-underscanning'])

    @property
    def is_builtin(self):
        """Return true if the display is builtin the machine (a laptop or a tablet)"""
        return bool(self._monitor[2]['is-builtin'])

    @property
    def display_name(self):
        """Human readable name of the display"""
        return str(self._monitor[2]['display-name'])
display_name property readonly

Human readable name of the display

is_builtin property readonly

Return true if the display is builtin the machine (a laptop or a tablet)

is_underscanning property readonly

Return true if the monitor is underscanning

model property readonly

Model name of the monitor

name property readonly

Name of the connector

serial_number property readonly

Serial number

vendor property readonly

Manufacturer of the monitor

__init__(self, monitor) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, monitor):
    self._monitor = monitor
get_current_mode(self)

Return the current mode

Source code in lutris/util/graphics/displayconfig.py
def get_current_mode(self):
    """Return the current mode"""
    for mode in self.get_modes():
        if mode.is_current:
            return mode
    return
get_mode_for_resolution(self, resolution)

Return an appropriate mode for a given resolution

Source code in lutris/util/graphics/displayconfig.py
def get_mode_for_resolution(self, resolution):
    """Return an appropriate mode for a given resolution"""
    width, height = [int(i) for i in resolution.split("x")]
    for mode in self.get_modes():
        if mode.width == width and mode.height == height:
            return mode
    return
get_modes(self)

Return available modes

Source code in lutris/util/graphics/displayconfig.py
def get_modes(self):
    """Return available modes"""
    return [MonitorMode(mode) for mode in self._monitor[1]]
MonitorMode (DisplayMode)

Represents a mode given by a Monitor instance In addition to DisplayMode objects, this gives acces to the current scaling used and some additional properties like is_current.

Source code in lutris/util/graphics/displayconfig.py
class MonitorMode(DisplayMode):

    """Represents a mode given by a Monitor instance
    In addition to DisplayMode objects, this gives acces to the current scaling
    used and some additional properties like is_current.
    """

    @property
    def width(self):
        """width in physical pixels"""
        return int(self.mode_info[1])

    @property
    def height(self):
        """height in physical pixels"""
        return int(self.mode_info[2])

    @property
    def frequency(self):
        """refresh rate"""
        return str(self.mode_info[3])

    @property
    def scale(self):
        """scale preferred as per calculations"""
        return float(self.mode_info[4])

    @property
    def supported_scale(self):
        """scales supported by this mode"""
        return self.mode_info[5]

    @property
    def properties(self):
        """Additional properties"""
        return self.mode_info[6]

    @property
    def is_current(self):
        """Return True if the mode is the current one"""
        return "is-current" in self.properties
frequency property readonly

refresh rate

height property readonly

height in physical pixels

is_current property readonly

Return True if the mode is the current one

properties property readonly

Additional properties

scale property readonly

scale preferred as per calculations

supported_scale property readonly

scales supported by this mode

width property readonly

width in physical pixels

MutterDisplayConfig

Class to interact with the Mutter.DisplayConfig service

Source code in lutris/util/graphics/displayconfig.py
class MutterDisplayConfig():

    """Class to interact with the Mutter.DisplayConfig service"""
    namespace = "org.gnome.Mutter.DisplayConfig"
    dbus_path = "/org/gnome/Mutter/DisplayConfig"

    # Methods used in ApplyMonitorConfig
    VERIFY_METHOD = 0
    TEMPORARY_METHOD = 1
    PERSISTENT_METHOD = 2

    def __init__(self):
        session_bus = dbus.SessionBus()
        proxy_obj = session_bus.get_object(self.namespace, self.dbus_path)
        self.interface = dbus.Interface(proxy_obj, dbus_interface=self.namespace)
        self.resources = self.interface.GetResources()
        self.current_state = DisplayState(self.interface)

    @property
    def serial(self):
        """
        @serial is an unique identifier representing the current state
        of the screen. It must be passed back to ApplyConfiguration()
        and will be increased for every configuration change (so that
        mutter can detect that the new configuration is based on old
        state)
        """
        return self.resources[0]

    @property
    def crtcs(self):
        """
        A CRTC (CRT controller) is a logical monitor, ie a portion
        of the compositor coordinate space. It might correspond
        to multiple monitors, when in clone mode, but not that
        it is possible to implement clone mode also by setting different
        CRTCs to the same coordinates.

        The number of CRTCs represent the maximum number of monitors
        that can be set to expand and it is a HW constraint; if more
        monitors are connected, then necessarily some will clone. This
        is complementary to the concept of the encoder (not exposed in
        the API), which groups outputs that necessarily will show the
        same image (again a HW constraint).

        A CRTC is represented by a DBus structure with the following
        layout:
        * u ID: the ID in the API of this CRTC
        * x winsys_id: the low-level ID of this CRTC (which might
                    be a XID, a KMS handle or something entirely
                    different)
        * i x, y, width, height: the geometry of this CRTC
                                (might be invalid if the CRTC is not in
                                use)
        * i current_mode: the current mode of the CRTC, or -1 if this
                        CRTC is not used
                        Note: the size of the mode will always correspond
                        to the width and height of the CRTC
        * u current_transform: the current transform (espressed according
                            to the wayland protocol)
        * au transforms: all possible transforms
        * a{sv} properties: other high-level properties that affect this
                            CRTC; they are not necessarily reflected in
                            the hardware.
                            No property is specified in this version of the API.

        Note: all geometry information refers to the untransformed
        display.
        """
        return [CRTC(crtc) for crtc in self.resources[1]]

    @property
    def outputs(self):
        """
        An output represents a physical screen, connected somewhere to
        the computer. Floating connectors are not exposed in the API.
        An output is a DBus struct with the following fields:
        * u ID: the ID in the API
        * x winsys_id: the low-level ID of this output (XID or KMS handle)
        * i current_crtc: the CRTC that is currently driving this output,
                          or -1 if the output is disabled
        * au possible_crtcs: all CRTCs that can control this output
        * s name: the name of the connector to which the output is attached
                  (like VGA1 or HDMI)
        * au modes: valid modes for this output
        * au clones: valid clones for this output, ie other outputs that
                     can be assigned the same CRTC as this one; if you
                     want to mirror two outputs that don't have each other
                     in the clone list, you must configure two different
                     CRTCs for the same geometry
        * a{sv} properties: other high-level properties that affect this
                            output; they are not necessarily reflected in
                            the hardware.
                            Known properties:
                            - "vendor" (s): (readonly) the human readable name
                                            of the manufacturer
                            - "product" (s): (readonly) the human readable name
                                             of the display model
                            - "serial" (s): (readonly) the serial number of this
                                            particular hardware part
                            - "display-name" (s): (readonly) a human readable name
                                                  of this output, to be shown in the UI
                            - "backlight" (i): (readonly, use the specific interface)
                                               the backlight value as a percentage
                                               (-1 if not supported)
                            - "primary" (b): whether this output is primary
                                             or not
                            - "presentation" (b): whether this output is
                                                  for presentation only
                            Note: properties might be ignored if not consistenly
                            applied to all outputs in the same clone group. In
                            general, it's expected that presentation or primary
                            outputs will not be cloned.
        """
        return [Output(output) for output in self.resources[2]]

    @property
    def modes(self):
        """
        A mode represents a set of parameters that are applied to
        each output, such as resolution and refresh rate. It is a separate
        object so that it can be referenced by CRTCs and outputs.
        Multiple outputs in the same CRTCs must all have the same mode.
        A mode is exposed as:
        * u ID: the ID in the API
        * x winsys_id: the low-level ID of this mode
        * u width, height: the resolution
        * d frequency: refresh rate
        * u flags: mode flags as defined in xf86drmMode.h and randr.h

        Output and modes are read-only objects (except for output properties),
        they can change only in accordance to HW changes (such as hotplugging
        a monitor), while CRTCs can be changed with ApplyConfiguration().

        XXX: actually, if you insist enough, you can add new modes
        through xrandr command line or the KMS API, overriding what the
        kernel driver and the EDID say.
        Usually, it only matters with old cards with broken drivers, or
        old monitors with broken EDIDs, but it happens more often with
        projectors (if for example the kernel driver doesn't add the
        640x480 - 800x600 - 1024x768 default modes). Probably something
        that we need to handle in mutter anyway.
        """
        return [DisplayMode(mode) for mode in self.resources[3]]

    @property
    def max_screen_width(self):
        """Maximum width supported"""
        return self.resources[4]

    @property
    def max_screen_height(self):
        """Maximum height supported"""
        return self.resources[5]

    def get_mode_for_resolution(self, resolution):
        """Return an appropriate mode for a given resolution"""
        width, height = [int(i) for i in resolution.split("x")]
        for mode in self.modes:
            if mode.width == width and mode.height == height:
                return mode
        return

    def get_primary_output(self):
        """Return the primary output"""
        for output in self.current_state.logical_monitors:
            if output.primary:
                return output
        return

    def apply_monitors_config(self, display_configs):
        """Set the selected display to the desired resolution"""
        # Reload resources
        if not display_configs:
            logger.error("No display config given, not applying anything")
            return
        self.resources = self.interface.GetResources()
        self.current_state = DisplayState(self.interface)
        monitors_config = [
            [
                config.position[0], config.position[1],
                dbus.Double(config.scale),
                dbus.UInt32(config.transform), config.primary,
                [
                    [dbus.String(str(display_name)), dbus.String(str(mode)), {}]
                    for display_name, mode in config.monitors
                ]
            ] for config in display_configs
        ]
        self.interface.ApplyMonitorsConfig(self.current_state.serial, self.TEMPORARY_METHOD, monitors_config, {})
PERSISTENT_METHOD
TEMPORARY_METHOD
VERIFY_METHOD
crtcs property readonly

A CRTC (CRT controller) is a logical monitor, ie a portion of the compositor coordinate space. It might correspond to multiple monitors, when in clone mode, but not that it is possible to implement clone mode also by setting different CRTCs to the same coordinates.

The number of CRTCs represent the maximum number of monitors that can be set to expand and it is a HW constraint; if more monitors are connected, then necessarily some will clone. This is complementary to the concept of the encoder (not exposed in the API), which groups outputs that necessarily will show the same image (again a HW constraint).

A CRTC is represented by a DBus structure with the following layout: * u ID: the ID in the API of this CRTC * x winsys_id: the low-level ID of this CRTC (which might be a XID, a KMS handle or something entirely different) * i x, y, width, height: the geometry of this CRTC (might be invalid if the CRTC is not in use) * i current_mode: the current mode of the CRTC, or -1 if this CRTC is not used Note: the size of the mode will always correspond to the width and height of the CRTC * u current_transform: the current transform (espressed according to the wayland protocol) * au transforms: all possible transforms * a{sv} properties: other high-level properties that affect this CRTC; they are not necessarily reflected in the hardware. No property is specified in this version of the API.

Note: all geometry information refers to the untransformed display.

dbus_path
max_screen_height property readonly

Maximum height supported

max_screen_width property readonly

Maximum width supported

modes property readonly

A mode represents a set of parameters that are applied to each output, such as resolution and refresh rate. It is a separate object so that it can be referenced by CRTCs and outputs. Multiple outputs in the same CRTCs must all have the same mode. A mode is exposed as: * u ID: the ID in the API * x winsys_id: the low-level ID of this mode * u width, height: the resolution * d frequency: refresh rate * u flags: mode flags as defined in xf86drmMode.h and randr.h

Output and modes are read-only objects (except for output properties), they can change only in accordance to HW changes (such as hotplugging a monitor), while CRTCs can be changed with ApplyConfiguration().

XXX: actually, if you insist enough, you can add new modes through xrandr command line or the KMS API, overriding what the kernel driver and the EDID say. Usually, it only matters with old cards with broken drivers, or old monitors with broken EDIDs, but it happens more often with projectors (if for example the kernel driver doesn't add the 640x480 - 800x600 - 1024x768 default modes). Probably something that we need to handle in mutter anyway.

namespace
outputs property readonly

An output represents a physical screen, connected somewhere to the computer. Floating connectors are not exposed in the API. An output is a DBus struct with the following fields: * u ID: the ID in the API * x winsys_id: the low-level ID of this output (XID or KMS handle) * i current_crtc: the CRTC that is currently driving this output, or -1 if the output is disabled * au possible_crtcs: all CRTCs that can control this output * s name: the name of the connector to which the output is attached (like VGA1 or HDMI) * au modes: valid modes for this output * au clones: valid clones for this output, ie other outputs that can be assigned the same CRTC as this one; if you want to mirror two outputs that don't have each other in the clone list, you must configure two different CRTCs for the same geometry * a{sv} properties: other high-level properties that affect this output; they are not necessarily reflected in the hardware. Known properties: - "vendor" (s): (readonly) the human readable name of the manufacturer - "product" (s): (readonly) the human readable name of the display model - "serial" (s): (readonly) the serial number of this particular hardware part - "display-name" (s): (readonly) a human readable name of this output, to be shown in the UI - "backlight" (i): (readonly, use the specific interface) the backlight value as a percentage (-1 if not supported) - "primary" (b): whether this output is primary or not - "presentation" (b): whether this output is for presentation only Note: properties might be ignored if not consistenly applied to all outputs in the same clone group. In general, it's expected that presentation or primary outputs will not be cloned.

serial property readonly

@serial is an unique identifier representing the current state of the screen. It must be passed back to ApplyConfiguration() and will be increased for every configuration change (so that mutter can detect that the new configuration is based on old state)

__init__(self) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self):
    session_bus = dbus.SessionBus()
    proxy_obj = session_bus.get_object(self.namespace, self.dbus_path)
    self.interface = dbus.Interface(proxy_obj, dbus_interface=self.namespace)
    self.resources = self.interface.GetResources()
    self.current_state = DisplayState(self.interface)
apply_monitors_config(self, display_configs)

Set the selected display to the desired resolution

Source code in lutris/util/graphics/displayconfig.py
def apply_monitors_config(self, display_configs):
    """Set the selected display to the desired resolution"""
    # Reload resources
    if not display_configs:
        logger.error("No display config given, not applying anything")
        return
    self.resources = self.interface.GetResources()
    self.current_state = DisplayState(self.interface)
    monitors_config = [
        [
            config.position[0], config.position[1],
            dbus.Double(config.scale),
            dbus.UInt32(config.transform), config.primary,
            [
                [dbus.String(str(display_name)), dbus.String(str(mode)), {}]
                for display_name, mode in config.monitors
            ]
        ] for config in display_configs
    ]
    self.interface.ApplyMonitorsConfig(self.current_state.serial, self.TEMPORARY_METHOD, monitors_config, {})
get_mode_for_resolution(self, resolution)

Return an appropriate mode for a given resolution

Source code in lutris/util/graphics/displayconfig.py
def get_mode_for_resolution(self, resolution):
    """Return an appropriate mode for a given resolution"""
    width, height = [int(i) for i in resolution.split("x")]
    for mode in self.modes:
        if mode.width == width and mode.height == height:
            return mode
    return
get_primary_output(self)

Return the primary output

Source code in lutris/util/graphics/displayconfig.py
def get_primary_output(self):
    """Return the primary output"""
    for output in self.current_state.logical_monitors:
        if output.primary:
            return output
    return
MutterDisplayManager

Manage displays using the DBus Mutter interface

Source code in lutris/util/graphics/displayconfig.py
class MutterDisplayManager:

    """Manage displays using the DBus Mutter interface"""

    def __init__(self):
        self.display_config = MutterDisplayConfig()

    def get_config(self):
        """Return the current configuration for each logical monitor"""
        return [logical_monitor.get_config() for logical_monitor in self.display_config.current_state.logical_monitors]

    def get_display_names(self):
        """Return display names of connected displays"""
        return [output.display_name for output in self.display_config.outputs]

    def get_resolutions(self):
        """Return available resolutions"""
        resolutions = ["%sx%s" % (mode.width, mode.height) for mode in self.display_config.modes]
        return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)

    def get_current_resolution(self):
        """Return the current resolution for the primary display"""
        logger.debug("Retrieving current resolution")
        current_mode = self.display_config.current_state.get_current_mode()
        if not current_mode:
            logger.error("Could not retrieve the current display mode")
            return "", ""
        return str(current_mode.width), str(current_mode.height)

    def set_resolution(self, resolution):
        """Change the current resolution"""
        if isinstance(resolution, str):
            output = self.display_config.get_primary_output()
            mode = output.monitors[0].get_mode_for_resolution(resolution)
            if not mode:
                logger.error("Could not find  valid mode for %s", resolution)
                return
            config = [
                DisplayConfig([(output.monitors[0].name, mode.id)], output.monitors[0].name, (0, 0), 0, True, 1.0)
            ]
            self.display_config.apply_monitors_config(config)
        elif resolution:
            self.display_config.apply_monitors_config(resolution)
        else:
            return

        # Load a fresh config since the current one has changed
        self.display_config = MutterDisplayConfig()
__init__(self) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self):
    self.display_config = MutterDisplayConfig()
get_config(self)

Return the current configuration for each logical monitor

Source code in lutris/util/graphics/displayconfig.py
def get_config(self):
    """Return the current configuration for each logical monitor"""
    return [logical_monitor.get_config() for logical_monitor in self.display_config.current_state.logical_monitors]
get_current_resolution(self)

Return the current resolution for the primary display

Source code in lutris/util/graphics/displayconfig.py
def get_current_resolution(self):
    """Return the current resolution for the primary display"""
    logger.debug("Retrieving current resolution")
    current_mode = self.display_config.current_state.get_current_mode()
    if not current_mode:
        logger.error("Could not retrieve the current display mode")
        return "", ""
    return str(current_mode.width), str(current_mode.height)
get_display_names(self)

Return display names of connected displays

Source code in lutris/util/graphics/displayconfig.py
def get_display_names(self):
    """Return display names of connected displays"""
    return [output.display_name for output in self.display_config.outputs]
get_resolutions(self)

Return available resolutions

Source code in lutris/util/graphics/displayconfig.py
def get_resolutions(self):
    """Return available resolutions"""
    resolutions = ["%sx%s" % (mode.width, mode.height) for mode in self.display_config.modes]
    return sorted(set(resolutions), key=lambda x: int(x.split("x")[0]), reverse=True)
set_resolution(self, resolution)

Change the current resolution

Source code in lutris/util/graphics/displayconfig.py
def set_resolution(self, resolution):
    """Change the current resolution"""
    if isinstance(resolution, str):
        output = self.display_config.get_primary_output()
        mode = output.monitors[0].get_mode_for_resolution(resolution)
        if not mode:
            logger.error("Could not find  valid mode for %s", resolution)
            return
        config = [
            DisplayConfig([(output.monitors[0].name, mode.id)], output.monitors[0].name, (0, 0), 0, True, 1.0)
        ]
        self.display_config.apply_monitors_config(config)
    elif resolution:
        self.display_config.apply_monitors_config(resolution)
    else:
        return

    # Load a fresh config since the current one has changed
    self.display_config = MutterDisplayConfig()
Output

Representation of a physical display output

Source code in lutris/util/graphics/displayconfig.py
class Output:

    """Representation of a physical display output"""

    def __init__(self, output_info):
        self._output = output_info

    def __repr__(self):
        return "<Output: %s %s (%s)>" % (self.vendor, self.product, self.display_name)

    @property
    def output_id(self):
        """ID of the output"""
        return self._output[0]

    @property
    def winsys_id(self):
        """The low-level ID of this output (XID or KMS handle)"""
        return self._output[1]

    @property
    def current_crtc(self):
        """The CRTC that is currently driving this output,
        or -1 if the output is disabled
        """
        return self._output[2]

    @property
    def crtcs(self):
        """All CRTCs that can control this output"""
        return self._output[3]

    @property
    def name(self):
        """The name of the connector to which the output is attached (like VGA1 or HDMI)"""
        return self._output[4]

    @property
    def modes(self):
        """Valid modes for this output"""
        return [int(mode_id) for mode_id in self._output[5]]

    @property
    def clones(self):
        """Valid clones for this output, ie other outputs that can be assigned
        the same CRTC as this one; if you want to mirror two outputs that don't
        have each other in the clone list, you must configure two different
        CRTCs for the same geometry.
        """
        return self._output[6]

    @property
    def properties(self):
        """Other high-level properties that affect this output; they are not
        necessarily reflected in the hardware.
        """
        return self._output[7]

    @property
    def vendor(self):
        """Vendor name of the output"""
        return str(self._output[7]["vendor"])

    @property
    def product(self):
        """Product name of the output"""
        return str(self._output[7]["product"])

    @property
    def display_name(self):
        """A human readable name of this output, to be shown in the UI"""
        return str(self._output[7]["display-name"])

    @property
    def is_primary(self):
        """True if the output is the primary one"""
        return bool(self._output[7]["primary"])
clones property readonly

Valid clones for this output, ie other outputs that can be assigned the same CRTC as this one; if you want to mirror two outputs that don't have each other in the clone list, you must configure two different CRTCs for the same geometry.

crtcs property readonly

All CRTCs that can control this output

current_crtc property readonly

The CRTC that is currently driving this output, or -1 if the output is disabled

display_name property readonly

A human readable name of this output, to be shown in the UI

is_primary property readonly

True if the output is the primary one

modes property readonly

Valid modes for this output

name property readonly

The name of the connector to which the output is attached (like VGA1 or HDMI)

output_id property readonly

ID of the output

product property readonly

Product name of the output

properties property readonly

Other high-level properties that affect this output; they are not necessarily reflected in the hardware.

vendor property readonly

Vendor name of the output

winsys_id property readonly

The low-level ID of this output (XID or KMS handle)

__init__(self, output_info) special
Source code in lutris/util/graphics/displayconfig.py
def __init__(self, output_info):
    self._output = output_info
__repr__(self) special
Source code in lutris/util/graphics/displayconfig.py
def __repr__(self):
    return "<Output: %s %s (%s)>" % (self.vendor, self.product, self.display_name)

drivers

Hardware driver related utilities

Everything in this module should rely on /proc or /sys only, no executable calls

check_driver()

Report on the currently running driver

Source code in lutris/util/graphics/drivers.py
def check_driver():
    """Report on the currently running driver"""
    if is_nvidia():
        driver_info = get_nvidia_driver_info()
        # pylint: disable=logging-format-interpolation
        logger.info("Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"]))
        gpus = get_nvidia_gpu_ids()
        for gpu_id in gpus:
            gpu_info = get_nvidia_gpu_info(gpu_id)
            logger.info("GPU: %s", gpu_info.get("Model"))
    for card in get_gpus():
        # pylint: disable=logging-format-interpolation
        logger.info("GPU: {PCI_ID} {PCI_SUBSYS_ID} using {DRIVER} driver".format(**get_gpu_info(card)))
get_gpu_info(card)

Return information about a GPU

Source code in lutris/util/graphics/drivers.py
def get_gpu_info(card):
    """Return information about a GPU"""
    infos = {"DRIVER": "", "PCI_ID": "", "PCI_SUBSYS_ID": ""}
    try:
        with open("/sys/class/drm/%s/device/uevent" % card, encoding='utf-8') as card_uevent:
            content = card_uevent.readlines()
    except FileNotFoundError:
        logger.error("Unable to read driver information for card %s", card)
        return infos
    for line in content:
        key, value = line.split("=", 1)
        infos[key] = value.strip()
    return infos
get_gpus()

Return GPUs connected to the system

Source code in lutris/util/graphics/drivers.py
def get_gpus():
    """Return GPUs connected to the system"""
    if not os.path.exists("/sys/class/drm"):
        logger.error("No GPU available on this system!")
        return
    for cardname in os.listdir("/sys/class/drm/"):
        if re.match(r"^card\d$", cardname):
            yield cardname
get_nvidia_driver_info()

Return information about NVidia drivers

Source code in lutris/util/graphics/drivers.py
def get_nvidia_driver_info():
    """Return information about NVidia drivers"""
    version_file_path = "/proc/driver/nvidia/version"
    if not os.path.exists(version_file_path):
        return
    with open(version_file_path, encoding='utf-8') as version_file:
        content = version_file.readlines()
        nvrm_version = content[0].split(': ')[1].strip().split()
        return {
            'nvrm': {
                'vendor': nvrm_version[0],
                'platform': nvrm_version[1],
                'arch': nvrm_version[2],
                'version': nvrm_version[5],
                'date': ' '.join(nvrm_version[6:])
            }
        }
    return
get_nvidia_gpu_ids()

Return the list of Nvidia GPUs

Source code in lutris/util/graphics/drivers.py
def get_nvidia_gpu_ids():
    """Return the list of Nvidia GPUs"""
    return os.listdir("/proc/driver/nvidia/gpus")
get_nvidia_gpu_info(gpu_id)

Return details about a GPU

Source code in lutris/util/graphics/drivers.py
def get_nvidia_gpu_info(gpu_id):
    """Return details about a GPU"""
    with open("/proc/driver/nvidia/gpus/%s/information" % gpu_id, encoding='utf-8') as info_file:
        content = info_file.readlines()
    infos = {}
    for line in content:
        key, value = line.split(":", 1)
        infos[key] = value.strip()
    return infos
is_amd()

Return true if the system uses the AMD driver

Source code in lutris/util/graphics/drivers.py
def is_amd():
    """Return true if the system uses the AMD driver"""
    for card in get_gpus():
        if get_gpu_info(card)["DRIVER"] == "amdgpu":
            return True
    return False
is_nvidia()

Return true if the Nvidia drivers are currently in use

Source code in lutris/util/graphics/drivers.py
def is_nvidia():
    """Return true if the Nvidia drivers are currently in use"""
    return os.path.exists("/proc/driver/nvidia")
is_outdated()
Source code in lutris/util/graphics/drivers.py
def is_outdated():
    if not is_nvidia():
        return False
    driver_info = get_nvidia_driver_info()
    driver_version = driver_info["nvrm"]["version"]
    if not driver_version:
        logger.error("Failed to get Nvidia version")
        return True
    major_version = int(driver_version.split(".")[0])
    return major_version < MIN_RECOMMENDED_NVIDIA_DRIVER

glxinfo

Parser for the glxinfo utility

Container

A dummy container for data

Source code in lutris/util/graphics/glxinfo.py
class Container:  # pylint: disable=too-few-public-methods
    """A dummy container for data"""
GlxInfo

Give access to the glxinfo information

Source code in lutris/util/graphics/glxinfo.py
class GlxInfo:
    """Give access to the glxinfo information"""

    def __init__(self, output=None):
        """Creates a new GlxInfo object

        Params:
            output (str): If provided, use this as the glxinfo output instead
                          of running the program, useful for testing.
        """
        self._output = output or self.get_glxinfo_output()
        self._section = None
        self._attrs = set()  # Keep a reference of the created attributes
        self.parse()

    @staticmethod
    def get_glxinfo_output():
        """Return the glxinfo -B output"""
        return read_process_output(["glxinfo", "-B"])

    def as_dict(self):
        """Return the attributes as a dict"""
        return {attr: getattr(self, attr) for attr in self._attrs}

    def parse(self):
        """Converts the glxinfo output to class attributes"""
        if not self._output:
            logger.error("No available glxinfo output")
            return
        # Fix glxinfo output (Great, you saved one line by
        # combining display and screen)
        output = self._output.replace("  screen", "\nscreen")
        for line in output.split("\n"):
            if not line.strip():
                continue

            key, value = line.split(":", 1)
            key = key.replace(" string", "").replace(" ", "_")
            value = value.strip()

            if not value and key.startswith(("Extended_renderer_info", "Memory_info")):
                self._section = key[key.index("(") + 1:-1]
                setattr(self, self._section, Container())
                continue
            if self._section:
                if not key.startswith("____"):
                    self._section = None
                else:
                    setattr(getattr(self, self._section), key.strip("_").lower(), value)
                    continue
            self._attrs.add(key.lower())
            setattr(self, key.lower(), value)
__init__(self, output=None) special

Creates a new GlxInfo object

Parameters:

Name Type Description Default
output str

If provided, use this as the glxinfo output instead of running the program, useful for testing.

None
Source code in lutris/util/graphics/glxinfo.py
def __init__(self, output=None):
    """Creates a new GlxInfo object

    Params:
        output (str): If provided, use this as the glxinfo output instead
                      of running the program, useful for testing.
    """
    self._output = output or self.get_glxinfo_output()
    self._section = None
    self._attrs = set()  # Keep a reference of the created attributes
    self.parse()
as_dict(self)

Return the attributes as a dict

Source code in lutris/util/graphics/glxinfo.py
def as_dict(self):
    """Return the attributes as a dict"""
    return {attr: getattr(self, attr) for attr in self._attrs}
get_glxinfo_output() staticmethod

Return the glxinfo -B output

Source code in lutris/util/graphics/glxinfo.py
@staticmethod
def get_glxinfo_output():
    """Return the glxinfo -B output"""
    return read_process_output(["glxinfo", "-B"])
parse(self)

Converts the glxinfo output to class attributes

Source code in lutris/util/graphics/glxinfo.py
def parse(self):
    """Converts the glxinfo output to class attributes"""
    if not self._output:
        logger.error("No available glxinfo output")
        return
    # Fix glxinfo output (Great, you saved one line by
    # combining display and screen)
    output = self._output.replace("  screen", "\nscreen")
    for line in output.split("\n"):
        if not line.strip():
            continue

        key, value = line.split(":", 1)
        key = key.replace(" string", "").replace(" ", "_")
        value = value.strip()

        if not value and key.startswith(("Extended_renderer_info", "Memory_info")):
            self._section = key[key.index("(") + 1:-1]
            setattr(self, self._section, Container())
            continue
        if self._section:
            if not key.startswith("____"):
                self._section = None
            else:
                setattr(getattr(self, self._section), key.strip("_").lower(), value)
                continue
        self._attrs.add(key.lower())
        setattr(self, key.lower(), value)

vkquery

Query Vulkan capabilities

VK_ERROR_INITIALIZATION_FAILED
VK_STRUCTURE_TYPE_APPLICATION_INFO
VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
VK_SUCCESS
VkInstance
VkInstanceCreateFlags
VkResult
VkStructureType
VkApplicationInfo (Structure)

Python shim for struct VkApplicationInfo

https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkApplicationInfo.html

Source code in lutris/util/graphics/vkquery.py
class VkApplicationInfo(Structure):

    """Python shim for struct VkApplicationInfo

    https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkApplicationInfo.html
    """

    # pylint: disable=too-few-public-methods

    _fields_ = [
        ("sType", VkStructureType),
        ("pNext", c_void_p),
        ("pApplicationName", c_char_p),
        ("applicationVersion", c_uint32),
        ("pEngineName", c_char_p),
        ("engineVersion", c_uint32),
        ("apiVersion", c_uint32),
    ]

    def __init__(self, name, version):
        super().__init__()
        self.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
        self.pApplicationName = name.encode()
        self.applicationVersion = vk_make_version(*version)
        self.apiVersion = vk_make_version(1, 0, 0)
__init__(self, name, version) special
Source code in lutris/util/graphics/vkquery.py
def __init__(self, name, version):
    super().__init__()
    self.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
    self.pApplicationName = name.encode()
    self.applicationVersion = vk_make_version(*version)
    self.apiVersion = vk_make_version(1, 0, 0)
VkInstanceCreateInfo (Structure)

Python shim for struct VkInstanceCreateInfo

https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkInstanceCreateInfo.html

Source code in lutris/util/graphics/vkquery.py
class VkInstanceCreateInfo(Structure):

    """Python shim for struct VkInstanceCreateInfo

    https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkInstanceCreateInfo.html
    """

    # pylint: disable=too-few-public-methods

    _fields_ = [
        ("sType", VkStructureType),
        ("pNext", c_void_p),
        ("flags", VkInstanceCreateFlags),
        ("pApplicationInfo", POINTER(VkApplicationInfo)),
        ("enabledLayerCount", c_uint32),
        ("ppEnabledLayerNames", c_char_p),
        ("enabledExtensionCount", c_uint32),
        ("ppEnabledExtensionNames", c_char_p),
    ]

    def __init__(self, app_info):
        super().__init__()
        self.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
        self.pApplicationInfo = pointer(app_info)
__init__(self, app_info) special
Source code in lutris/util/graphics/vkquery.py
def __init__(self, app_info):
    super().__init__()
    self.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
    self.pApplicationInfo = pointer(app_info)
is_vulkan_supported()

Returns True iff vulkan library can be loaded, initialized, and reports at least one physical device available.

Source code in lutris/util/graphics/vkquery.py
def is_vulkan_supported():
    """
    Returns True iff vulkan library can be loaded, initialized,
    and reports at least one physical device available.
    """
    vulkan = None
    try:
        vulkan = CDLL("libvulkan.so.1")
    except OSError:
        return False
    app_info = VkApplicationInfo("vkinfo", version=(0, 1, 0))
    create_info = VkInstanceCreateInfo(app_info)
    instance = VkInstance()
    result = vulkan.vkCreateInstance(byref(create_info), 0, byref(instance))
    if result != VK_SUCCESS:
        return False
    dev_count = c_uint32(0)
    result = vulkan.vkEnumeratePhysicalDevices(instance, byref(dev_count), 0)
    vulkan.vkDestroyInstance(instance, 0)
    return result == VK_SUCCESS and dev_count.value > 0
vk_make_version(major, minor, patch)

VK_MAKE_VERSION macro logic for Python

https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#fundamentals-versionnum

Source code in lutris/util/graphics/vkquery.py
def vk_make_version(major, minor, patch):
    """
    VK_MAKE_VERSION macro logic for Python

    https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#fundamentals-versionnum
    """
    return c_uint32((major << 22) | (minor << 12) | patch)

xephyr

Xephyr utilities

get_xephyr_command(display, config)

Return a configured Xephyr command

Source code in lutris/util/graphics/xephyr.py
def get_xephyr_command(display, config):
    """Return a configured Xephyr command"""
    xephyr_depth = "8" if config.get("xephyr") == "8bpp" else "16"
    xephyr_resolution = config.get("xephyr_resolution") or "640x480"
    xephyr_command = [
        "Xephyr",
        display,
        "-ac",
        "-screen",
        xephyr_resolution + "x" + xephyr_depth,
        "-glamor",
        "-reset",
        "-terminate",
    ]
    if config.get("xephyr_fullscreen"):
        xephyr_command.append("-fullscreen")
    return xephyr_command

xrandr

XrandR based display management

LegacyDisplayManager

Legacy XrandR based display manager. Does not work on Wayland.

Source code in lutris/util/graphics/xrandr.py
class LegacyDisplayManager:  # pylint: disable=too-few-public-methods

    """Legacy XrandR based display manager.
    Does not work on Wayland.
    """

    @staticmethod
    def get_display_names():
        """Return output names from XrandR"""
        return [output.name for output in get_outputs()]

    @staticmethod
    def get_resolutions():
        """Return available resolutions"""
        return get_resolutions()

    @staticmethod
    def get_current_resolution():
        """Return the current resolution for the desktop"""
        for line in _get_vidmodes():
            if line.startswith("  ") and "*" in line:
                resolution_match = re.match(r".*?(\d+x\d+).*", line)
                if resolution_match:
                    return resolution_match.groups()[0].split("x")
        return ("", "")

    @staticmethod
    def set_resolution(resolution):
        """Change the current resolution"""
        change_resolution(resolution)

    @staticmethod
    def get_config():
        """Return the current display configuration"""
        return get_outputs()
get_config() staticmethod

Return the current display configuration

Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_config():
    """Return the current display configuration"""
    return get_outputs()
get_current_resolution() staticmethod

Return the current resolution for the desktop

Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_current_resolution():
    """Return the current resolution for the desktop"""
    for line in _get_vidmodes():
        if line.startswith("  ") and "*" in line:
            resolution_match = re.match(r".*?(\d+x\d+).*", line)
            if resolution_match:
                return resolution_match.groups()[0].split("x")
    return ("", "")
get_display_names() staticmethod

Return output names from XrandR

Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_display_names():
    """Return output names from XrandR"""
    return [output.name for output in get_outputs()]
get_resolutions() staticmethod

Return available resolutions

Source code in lutris/util/graphics/xrandr.py
@staticmethod
def get_resolutions():
    """Return available resolutions"""
    return get_resolutions()
set_resolution(resolution) staticmethod

Change the current resolution

Source code in lutris/util/graphics/xrandr.py
@staticmethod
def set_resolution(resolution):
    """Change the current resolution"""
    change_resolution(resolution)
Output (tuple)

Output(name, mode, position, rotation, primary, rate)

__getnewargs__(self) special

Return self as a plain tuple. Used by copy and pickle.

Source code in lutris/util/graphics/xrandr.py
def __getnewargs__(self):
    'Return self as a plain tuple.  Used by copy and pickle.'
    return _tuple(self)
__new__(_cls, name, mode, position, rotation, primary, rate) special staticmethod

Create new instance of Output(name, mode, position, rotation, primary, rate)

__repr__(self) special

Return a nicely formatted representation string

Source code in lutris/util/graphics/xrandr.py
def __repr__(self):
    'Return a nicely formatted representation string'
    return self.__class__.__name__ + repr_fmt % self
change_resolution(resolution)

Change display resolution.

Takes a string for single monitors or a list of displays as returned by get_outputs().

Source code in lutris/util/graphics/xrandr.py
def change_resolution(resolution):
    """Change display resolution.

    Takes a string for single monitors or a list of displays as returned
    by get_outputs().
    """
    if not resolution:
        logger.warning("No resolution provided")
        return
    if isinstance(resolution, str):
        logger.debug("Switching resolution to %s", resolution)

        if resolution not in get_resolutions():
            logger.warning("Resolution %s doesn't exist.", resolution)
        else:
            logger.info("Changing resolution to %s", resolution)
            with subprocess.Popen([LINUX_SYSTEM.get("xrandr"), "-s", resolution]) as xrandr:
                xrandr.communicate()

    else:
        for display in resolution:
            logger.debug("Switching to %s on %s", display.mode, display.name)

            if display.rotation is not None and display.rotation in (
                "normal",
                "left",
                "right",
                "inverted",
            ):
                rotation = display.rotation
            else:
                rotation = "normal"
            logger.info("Switching resolution of %s to %s", display.name, display.mode)
            with subprocess.Popen(
                [
                    LINUX_SYSTEM.get("xrandr"),
                    "--output",
                    display.name,
                    "--mode",
                    display.mode,
                    "--pos",
                    display.position,
                    "--rotate",
                    rotation,
                    "--rate",
                    display.rate,
                ]
            ) as xrandr:
                xrandr.communicate()
get_outputs()

Return list of namedtuples containing output 'name', 'geometry', 'rotation' and whether it is the 'primary' display.

Source code in lutris/util/graphics/xrandr.py
def get_outputs():  # pylint: disable=too-many-locals
    """Return list of namedtuples containing output 'name', 'geometry',
    'rotation' and whether it is the 'primary' display."""
    outputs = []
    vid_modes = _get_vidmodes()
    position = None
    rotate = None
    primary = None
    name = None
    if not vid_modes:
        logger.error("xrandr didn't return anything")
        return []
    for line in vid_modes:
        if "connected" in line:
            if "disconnected" in line:
                continue
            primary = "primary" in line
            try:
                if primary:
                    name, _, _, geometry, rotate, *_ = line.split()
                else:
                    name, _, geometry, rotate, *_ = line.split()
            except ValueError as ex:
                logger.error(
                    "Unhandled xrandr line %s, error: %s. "
                    "Please send your xrandr output to the dev team", line, ex
                )
                continue
            if geometry.startswith("("):  # Screen turned off, no geometry
                continue
            if rotate.startswith("("):  # Screen not rotated, no need to include
                rotate = "normal"
            _, x_pos, y_pos = geometry.split("+")
            position = "{x_pos}x{y_pos}".format(x_pos=x_pos, y_pos=y_pos)
        elif "*" in line:
            mode, *framerates = line.split()
            for number in framerates:
                if "*" in number:
                    hertz = number[:-2]
                    outputs.append(
                        Output(
                            name=name,
                            mode=mode,
                            position=position,
                            rotation=rotate,
                            primary=primary,
                            rate=hertz,
                        )
                    )
                    break
    return outputs
get_resolutions()

Return the list of supported screen resolutions.

Source code in lutris/util/graphics/xrandr.py
def get_resolutions():
    """Return the list of supported screen resolutions."""
    resolution_list = []
    for line in _get_vidmodes():
        if line.startswith("  "):
            resolution_match = re.match(r".*?(\d+x\d+).*", line)
            if resolution_match:
                resolution_list.append(resolution_match.groups()[0])
    return resolution_list
get_unique_resolutions()

Return available resolutions, without duplicates and ordered with highest resolution first

Source code in lutris/util/graphics/xrandr.py
def get_unique_resolutions():
    """Return available resolutions, without duplicates and ordered with highest resolution first"""
    return sorted(set(get_resolutions()), key=lambda x: int(x.split("x")[0]), reverse=True)
turn_off_except(display)

Use XrandR to turn off displays except the one referenced by display

Source code in lutris/util/graphics/xrandr.py
def turn_off_except(display):
    """Use XrandR to turn off displays except the one referenced by `display`"""
    if not display:
        logger.error("No active display given, no turning off every display")
        return
    for output in get_outputs():
        if output.name != display:
            logger.info("Turning off %s", output[0])
            with subprocess.Popen([LINUX_SYSTEM.get("xrandr"), "--output", output.name, "--off"]) as xrandr:
                xrandr.communicate()

http

HTTP utilities

DEFAULT_TIMEOUT

HTTPError (Exception)

Exception raised on request failures

Source code in lutris/util/http.py
class HTTPError(Exception):
    """Exception raised on request failures"""

    def __init__(self, message, code=None):
        super().__init__(message)
        self.code = code
__init__(self, message, code=None) special
Source code in lutris/util/http.py
def __init__(self, message, code=None):
    super().__init__(message)
    self.code = code

Request

Source code in lutris/util/http.py
class Request:

    def __init__(
        self,
        url,
        timeout=DEFAULT_TIMEOUT,
        stop_request=None,
        headers=None,
        cookies=None,
    ):
        self.url = self._clean_url(url)
        self.status_code = None
        self.content = b""
        self.timeout = timeout
        self.stop_request = stop_request
        self.buffer_size = 1024 * 1024  # Bytes
        self.total_size = None
        self.downloaded_size = 0
        self.headers = {"User-Agent": self.user_agent}
        self.response_headers = None
        self.info = None
        if headers is None:
            headers = {}
        if not isinstance(headers, dict):
            raise TypeError("HTTP headers needs to be a dict ({})".format(headers))
        self.headers.update(headers)
        if cookies:
            cookie_processor = urllib.request.HTTPCookieProcessor(cookies)
            self.opener = urllib.request.build_opener(cookie_processor)
        else:
            self.opener = None

    @staticmethod
    def _clean_url(url):
        """Checks that a given URL is valid and return a usable version"""
        if not url:
            raise ValueError("An URL is required!")
        if url == "None":
            raise ValueError("You'd better stop that right now.")
        if url.startswith("//"):
            url = "https:" + url
        if url.startswith("/"):
            logger.error("Stop using relative URLs!: %s", url)
            url = SITE_URL + url
        # That's for a single URL in EGS... not sure if we need more escaping
        # The url received should already be receiving an escaped string
        url = url.replace(" ", "%20")
        return url

    @property
    def user_agent(self):
        return "{} {}".format(PROJECT, VERSION)

    def get(self, data=None):
        logger.debug("GET %s", self.url)
        try:
            req = urllib.request.Request(url=self.url, data=data, headers=self.headers)
        except ValueError as ex:
            raise HTTPError("Failed to create HTTP request to %s: %s" % (self.url, ex)) from ex
        try:
            if self.opener:
                request = self.opener.open(req, timeout=self.timeout)
            else:
                request = urllib.request.urlopen(req, timeout=self.timeout)  # pylint: disable=consider-using-with
        except (urllib.error.HTTPError, CertificateError) as error:
            if error.code == 401:
                raise UnauthorizedAccess("Access to %s denied" % self.url) from error
            raise HTTPError("%s" % error, code=error.code) from error
        except (socket.timeout, urllib.error.URLError) as error:
            raise HTTPError("Unable to connect to server %s: %s" % (self.url, error)) from error

        self.response_headers = request.getheaders()
        self.status_code = request.getcode()
        if self.status_code > 299:
            logger.warning("Request responded with code %s", self.status_code)

        try:
            self.total_size = int(request.info().get("Content-Length").strip())
        except AttributeError:
            self.total_size = 0

        self.content = b"".join(self._iter_chunks(request))
        self.info = request.info()
        request.close()
        return self

    def _iter_chunks(self, request):
        while 1:
            if self.stop_request and self.stop_request.is_set():
                self.content = b""
                return self
            try:
                chunk = request.read(self.buffer_size)
            except (socket.timeout, ConnectionResetError) as err:
                raise HTTPError("Request timed out") from err
            self.downloaded_size += len(chunk)
            if not chunk:
                return
            yield chunk

    def post(self, data):
        raise NotImplementedError

    def write_to_file(self, path):
        content = self.content
        logger.debug("Writing to %s", path)
        if not content:
            logger.warning("No content to write")
            return
        dirname = os.path.dirname(path)
        if not system.path_exists(dirname):
            os.makedirs(dirname)
        with open(path, "wb") as dest_file:
            dest_file.write(content)

    @property
    def json(self):
        _raw_json = self.text
        if _raw_json:
            try:
                return json.loads(_raw_json)
            except json.decoder.JSONDecodeError as err:
                raise ValueError(f"JSON response from {self.url} could not be decoded: '{_raw_json[:80]}'") from err
        return {}

    @property
    def text(self):
        if self.content:
            return self.content.decode()
        return ""
json property readonly
text property readonly
user_agent property readonly
__init__(self, url, timeout=30, stop_request=None, headers=None, cookies=None) special
Source code in lutris/util/http.py
def __init__(
    self,
    url,
    timeout=DEFAULT_TIMEOUT,
    stop_request=None,
    headers=None,
    cookies=None,
):
    self.url = self._clean_url(url)
    self.status_code = None
    self.content = b""
    self.timeout = timeout
    self.stop_request = stop_request
    self.buffer_size = 1024 * 1024  # Bytes
    self.total_size = None
    self.downloaded_size = 0
    self.headers = {"User-Agent": self.user_agent}
    self.response_headers = None
    self.info = None
    if headers is None:
        headers = {}
    if not isinstance(headers, dict):
        raise TypeError("HTTP headers needs to be a dict ({})".format(headers))
    self.headers.update(headers)
    if cookies:
        cookie_processor = urllib.request.HTTPCookieProcessor(cookies)
        self.opener = urllib.request.build_opener(cookie_processor)
    else:
        self.opener = None
get(self, data=None)
Source code in lutris/util/http.py
def get(self, data=None):
    logger.debug("GET %s", self.url)
    try:
        req = urllib.request.Request(url=self.url, data=data, headers=self.headers)
    except ValueError as ex:
        raise HTTPError("Failed to create HTTP request to %s: %s" % (self.url, ex)) from ex
    try:
        if self.opener:
            request = self.opener.open(req, timeout=self.timeout)
        else:
            request = urllib.request.urlopen(req, timeout=self.timeout)  # pylint: disable=consider-using-with
    except (urllib.error.HTTPError, CertificateError) as error:
        if error.code == 401:
            raise UnauthorizedAccess("Access to %s denied" % self.url) from error
        raise HTTPError("%s" % error, code=error.code) from error
    except (socket.timeout, urllib.error.URLError) as error:
        raise HTTPError("Unable to connect to server %s: %s" % (self.url, error)) from error

    self.response_headers = request.getheaders()
    self.status_code = request.getcode()
    if self.status_code > 299:
        logger.warning("Request responded with code %s", self.status_code)

    try:
        self.total_size = int(request.info().get("Content-Length").strip())
    except AttributeError:
        self.total_size = 0

    self.content = b"".join(self._iter_chunks(request))
    self.info = request.info()
    request.close()
    return self
post(self, data)
Source code in lutris/util/http.py
def post(self, data):
    raise NotImplementedError
write_to_file(self, path)
Source code in lutris/util/http.py
def write_to_file(self, path):
    content = self.content
    logger.debug("Writing to %s", path)
    if not content:
        logger.warning("No content to write")
        return
    dirname = os.path.dirname(path)
    if not system.path_exists(dirname):
        os.makedirs(dirname)
    with open(path, "wb") as dest_file:
        dest_file.write(content)

UnauthorizedAccess (Exception)

Exception raised for 401 HTTP errors

Source code in lutris/util/http.py
class UnauthorizedAccess(Exception):
    """Exception raised for 401 HTTP errors"""

download_file(url, dest, overwrite=False, raise_errors=False)

Save a remote resource locally

Source code in lutris/util/http.py
def download_file(url, dest, overwrite=False, raise_errors=False):
    """Save a remote resource locally"""
    if system.path_exists(dest):
        if overwrite:
            os.remove(dest)
        else:
            return dest
    if not url:
        return None
    try:
        request = Request(url).get()
    except HTTPError as ex:
        if raise_errors:
            raise
        logger.error("Failed to get url %s: %s", url, ex)
        return None
    request.write_to_file(dest)
    return dest

i18n

Language and translation utilities

get_lang()

Return the 2 letter language code used by the system

Source code in lutris/util/i18n.py
def get_lang():
    """Return the 2 letter language code used by the system"""
    user_locale = get_user_locale()
    if not user_locale:
        return ""
    return user_locale[:2]

get_lang_and_country()

Return language code and country for the current user

Source code in lutris/util/i18n.py
def get_lang_and_country():
    """Return language code and country for the current user"""
    user_locale = get_user_locale()
    if not user_locale:
        return "", ""
    lang_code, country = user_locale.split('-' if '-' in locale else '_')
    return lang_code, country

get_user_locale()

Source code in lutris/util/i18n.py
def get_user_locale():
    user_locale, _user_encoding = locale.getlocale()
    if not user_locale:
        logger.error("Unable to get locale")
        return
    return user_locale

jobs

AsyncCall (Thread)

Source code in lutris/util/jobs.py
class AsyncCall(threading.Thread):

    def __init__(self, func, callback, *args, **kwargs):
        """Execute `function` in a new thread then schedule `callback` for
        execution in the main loop.
        """
        self.source_id = None
        self.stop_request = threading.Event()

        super().__init__(target=self.target, args=args, kwargs=kwargs)
        self.function = func
        self.callback = callback if callback else lambda r, e: None
        self.daemon = kwargs.pop("daemon", True)

        self.start()

    def target(self, *args, **kwargs):
        result = None
        error = None

        try:
            result = self.function(*args, **kwargs)
        except Exception as ex:  # pylint: disable=broad-except
            logger.error("Error while completing task %s: %s %s", self.function, type(ex), ex)
            error = ex
            _ex_type, _ex_value, trace = sys.exc_info()
            traceback.print_tb(trace)

        self.source_id = GLib.idle_add(self.callback, result, error)
        return self.source_id
__init__(self, func, callback, *args, **kwargs) special

Execute function in a new thread then schedule callback for execution in the main loop.

Source code in lutris/util/jobs.py
def __init__(self, func, callback, *args, **kwargs):
    """Execute `function` in a new thread then schedule `callback` for
    execution in the main loop.
    """
    self.source_id = None
    self.stop_request = threading.Event()

    super().__init__(target=self.target, args=args, kwargs=kwargs)
    self.function = func
    self.callback = callback if callback else lambda r, e: None
    self.daemon = kwargs.pop("daemon", True)

    self.start()
target(self, *args, **kwargs)
Source code in lutris/util/jobs.py
def target(self, *args, **kwargs):
    result = None
    error = None

    try:
        result = self.function(*args, **kwargs)
    except Exception as ex:  # pylint: disable=broad-except
        logger.error("Error while completing task %s: %s %s", self.function, type(ex), ex)
        error = ex
        _ex_type, _ex_value, trace = sys.exc_info()
        traceback.print_tb(trace)

    self.source_id = GLib.idle_add(self.callback, result, error)
    return self.source_id

synchronized_call(func, event, result)

Calls func, stores the result by reference, set an event when finished

Source code in lutris/util/jobs.py
def synchronized_call(func, event, result):
    """Calls func, stores the result by reference, set an event when finished"""
    result.append(func())
    event.set()

thread_safe_call(func)

Synchronous call to func, safe to call in a callback started from a thread Not safe to use otherwise, will crash if run from the main thread.

See: https://pygobject.readthedocs.io/en/latest/guide/threading.html

Source code in lutris/util/jobs.py
def thread_safe_call(func):
    """Synchronous call to func, safe to call in a callback started from a thread
    Not safe to use otherwise, will crash if run from the main thread.

    See: https://pygobject.readthedocs.io/en/latest/guide/threading.html
    """
    event = threading.Event()
    result = []
    GLib.idle_add(synchronized_call, func, event, result)
    event.wait()
    return result[0]

joypad

get_controller_mappings()

Source code in lutris/util/joypad.py
def get_controller_mappings():
    devices = get_devices()
    controller_db = GameControllerDB()

    controllers = []

    for device in devices:
        guid = get_sdl_identifier(device.info)
        if guid in controller_db.controllers:
            controllers.append((device, controller_db[guid]))

    return controllers

get_devices()

Source code in lutris/util/joypad.py
def get_devices():
    if not evdev:
        logger.warning("python3-evdev not installed, controller support not available")
        return []
    _devices = []
    for dev in evdev.list_devices():
        try:
            _devices.append(evdev.InputDevice(dev))
        except RuntimeError:
            pass
    return _devices

get_joypads()

Return a list of tuples with the device and the joypad name

Source code in lutris/util/joypad.py
def get_joypads():
    """Return a list of tuples with the device and the joypad name"""
    return [(dev.fn, dev.name) for dev in get_devices()]

get_sdl_identifier(device_info)

Source code in lutris/util/joypad.py
def get_sdl_identifier(device_info):
    device_identifier = struct.pack(
        "<LLLL",
        device_info.bustype,
        device_info.vendor,
        device_info.product,
        device_info.version,
    )
    return binascii.hexlify(device_identifier).decode()

read_button(device)

Reference function for reading controller buttons and axis values. Not to be used as is.

Source code in lutris/util/joypad.py
def read_button(device):
    """Reference function for reading controller buttons and axis values.
    Not to be used as is.
    """
    # pylint: disable=no-member
    for event in device.read_loop():
        if event.type == evdev.ecodes.EV_KEY and event.value == 0:
            print("button %s (%s): %s" % (event.code, hex(event.code), event.value))
        if event.type == evdev.ecodes.EV_ABS:
            sticks = (0, 1, 3, 4)
            if event.code not in sticks or abs(event.value) > 5000:
                print("axis %s (%s): %s" % (event.code, hex(event.code), event.value))

keyring

KEYRING_NAME

store_credentials(username, password)

Source code in lutris/util/keyring.py
def store_credentials(username, password):
    try:
        keyring.set_password("Lutris", username, password)
        return True
    except PasswordSetError:
        return False

libretro

RetroConfig

Source code in lutris/util/libretro.py
class RetroConfig:
    value_map = {"true": True, "false": False, "": None}

    def __init__(self, config_path):
        if not config_path:
            raise ValueError("Config path is mandatory")
        self.config_path = config_path
        self._config = []

    @property
    def config(self):
        """Lazy loading of the RetroArch config """
        if self._config:
            return self._config
        try:
            self.load_config()
            return self._config
        except UnicodeDecodeError:
            logger.error(
                "The Retroarch config in %s could not "
                "be read because of character encoding issues",
                self.config_path
            )
            return []

    def load_config(self):
        """Load the configuration from file"""
        self._config = []
        if not os.path.isfile(self.config_path):
            raise OSError("Specified config file {} does not exist".format(self.config_path))
        with open(self.config_path, "r", encoding='utf-8') as config_file:
            for line in config_file.readlines():
                if not line:
                    continue
                line = line.strip()
                if line == "" or line.startswith('#'):
                    continue
                if '=' in line:
                    key, value = line.split("=", 1)
                    key = key.strip()
                    value = value.strip().strip('"')
                    if not key or not value:
                        continue
                    self._config.append((key, value))

    def save(self):
        with open(self.config_path, "w", encoding='utf-8') as config_file:
            for (key, value) in self.config:
                config_file.write('{} = "{}"\n'.format(key, value))

    def serialize_value(self, value):
        for k, v in self.value_map.items():
            if value is v:
                return k
        return value

    def deserialize_value(self, value):
        for k, v in self.value_map.items():
            if value == k:
                return v
        return value

    def __getitem__(self, key):
        for k, value in self.config:
            if key == k:
                return self.deserialize_value(value)

    def __setitem__(self, key, value):
        for index, conf in enumerate(self.config):
            if key == conf[0]:
                # self.config is read-only
                self._config[index] = (key, self.serialize_value(value))
                return
        self._config.append((key, self.serialize_value(value)))

    def keys(self):
        return [key for (key, _value) in self.config]
config property readonly

Lazy loading of the RetroArch config

value_map
__getitem__(self, key) special
Source code in lutris/util/libretro.py
def __getitem__(self, key):
    for k, value in self.config:
        if key == k:
            return self.deserialize_value(value)
__init__(self, config_path) special
Source code in lutris/util/libretro.py
def __init__(self, config_path):
    if not config_path:
        raise ValueError("Config path is mandatory")
    self.config_path = config_path
    self._config = []
__setitem__(self, key, value) special
Source code in lutris/util/libretro.py
def __setitem__(self, key, value):
    for index, conf in enumerate(self.config):
        if key == conf[0]:
            # self.config is read-only
            self._config[index] = (key, self.serialize_value(value))
            return
    self._config.append((key, self.serialize_value(value)))
deserialize_value(self, value)
Source code in lutris/util/libretro.py
def deserialize_value(self, value):
    for k, v in self.value_map.items():
        if value == k:
            return v
    return value
keys(self)
Source code in lutris/util/libretro.py
def keys(self):
    return [key for (key, _value) in self.config]
load_config(self)

Load the configuration from file

Source code in lutris/util/libretro.py
def load_config(self):
    """Load the configuration from file"""
    self._config = []
    if not os.path.isfile(self.config_path):
        raise OSError("Specified config file {} does not exist".format(self.config_path))
    with open(self.config_path, "r", encoding='utf-8') as config_file:
        for line in config_file.readlines():
            if not line:
                continue
            line = line.strip()
            if line == "" or line.startswith('#'):
                continue
            if '=' in line:
                key, value = line.split("=", 1)
                key = key.strip()
                value = value.strip().strip('"')
                if not key or not value:
                    continue
                self._config.append((key, value))
save(self)
Source code in lutris/util/libretro.py
def save(self):
    with open(self.config_path, "w", encoding='utf-8') as config_file:
        for (key, value) in self.config:
            config_file.write('{} = "{}"\n'.format(key, value))
serialize_value(self, value)
Source code in lutris/util/libretro.py
def serialize_value(self, value):
    for k, v in self.value_map.items():
        if value is v:
            return k
    return value

linux

Linux specific platform code

LINUX_SYSTEM

SYSTEM_COMPONENTS

linux_distribution

LinuxSystem

Global cache for system commands

Source code in lutris/util/linux.py
class LinuxSystem:  # pylint: disable=too-many-public-methods
    """Global cache for system commands"""

    _cache = {}

    multiarch_lib_folders = [
        ("/lib", "/lib64"),
        ("/lib32", "/lib64"),
        ("/usr/lib", "/usr/lib64"),
        ("/usr/lib32", "/usr/lib64"),
        ("/lib/i386-linux-gnu", "/lib/x86_64-linux-gnu"),
        ("/usr/lib/i386-linux-gnu", "/usr/lib/x86_64-linux-gnu"),
    ]

    soundfont_folders = [
        "/usr/share/sounds/sf2",
        "/usr/share/soundfonts",
    ]

    recommended_no_file_open = 524288
    required_components = ["OPENGL", "VULKAN", "GNUTLS"]
    optional_components = ["WINE", "GAMEMODE"]

    flatpak_info_path = "/.flatpak-info"

    def __init__(self):
        for key in ("COMMANDS", "TERMINALS"):
            self._cache[key] = {}
            for command in SYSTEM_COMPONENTS[key]:
                command_path = shutil.which(command)
                if not command_path:
                    command_path = self.get_sbin_path(command)
                if command_path:
                    self._cache[key][command] = command_path

        # Detect if system is 64bit capable
        self.is_64_bit = sys.maxsize > 2**32
        self.arch = self.get_arch()
        self.shared_libraries = self.get_shared_libraries()
        self.populate_libraries()
        self.populate_sound_fonts()
        self.soft_limit, self.hard_limit = self.get_file_limits()
        self.glxinfo = self.get_glxinfo()

    @staticmethod
    def get_sbin_path(command):
        """Some distributions don't put sbin directories in $PATH"""
        path_candidates = ["/sbin", "/usr/sbin"]
        for candidate in path_candidates:
            command_path = os.path.join(candidate, command)
            if os.path.exists(command_path):
                return command_path

    @staticmethod
    def get_file_limits():
        return resource.getrlimit(resource.RLIMIT_NOFILE)

    def has_enough_file_descriptors(self):
        return self.hard_limit >= self.recommended_no_file_open

    @staticmethod
    def get_cpus():
        """Parse the output of /proc/cpuinfo"""
        cpus = [{}]
        cpu_index = 0
        with open("/proc/cpuinfo", encoding='utf-8') as cpuinfo:
            for line in cpuinfo.readlines():
                if not line.strip():
                    cpu_index += 1
                    cpus.append({})
                    continue
                key, value = line.split(":", 1)
                cpus[cpu_index][key.strip()] = value.strip()
        return [cpu for cpu in cpus if cpu]

    @staticmethod
    def get_drives():
        """Return a list of drives with their filesystems"""
        lsblk_output = system.read_process_output(["lsblk", "-f", "--json"])
        return [
            drive
            for drive in json.loads(lsblk_output)["blockdevices"]
            if drive["fstype"] != "squashfs"
        ]

    @staticmethod
    def get_ram_info():
        """Parse the output of /proc/meminfo and return RAM information in kB"""
        mem = {}
        with open("/proc/meminfo", encoding='utf-8') as meminfo:
            for line in meminfo.readlines():
                key, value = line.split(":", 1)
                mem[key.strip()] = value.strip('kB \n')
        return mem

    @staticmethod
    def get_dist_info():
        """Return distribution information"""
        if linux_distribution:
            return linux_distribution()
        return "unknown"

    @staticmethod
    def get_arch():
        """Return the system architecture only if compatible
        with the supported architectures from the Lutris API
        """
        machine = platform.machine()
        if machine == "x86_64":
            return "x86_64"
        if machine in ("i386", "i686"):
            return "i386"
        if "armv7" in machine:
            return "armv7"
        logger.warning("Unsupported architecture %s", machine)

    @staticmethod
    def get_kernel_version():
        """Get kernel info from /proc/version"""
        with open("/proc/version", encoding='utf-8') as kernel_info:
            info = kernel_info.readlines()[0]
            version = info.split(" ")[2]
        return version

    def gamemode_available(self):
        """Return whether gamemode is available"""
        # Current versions of gamemode use gamemoderun
        if system.find_executable("gamemoderun"):
            return True
        # This is for old versions of gamemode only
        if self.is_feature_supported("GAMEMODE"):
            return True
        return False

    @property
    def has_steam(self):
        """Return whether Steam is installed locally"""
        return bool(system.find_executable("steam"))

    @property
    def display_server(self):
        """Return the display server used"""
        return os.environ.get("XDG_SESSION_TYPE", "unknown")

    @property
    def is_flatpak(self):
        """Check is we are running inside Flatpak sandbox"""
        return system.path_exists(self.flatpak_info_path)

    @property
    def runtime_architectures(self):
        """Return the architectures supported on this machine"""
        if self.arch == "x86_64":
            return ["i386", "x86_64"]
        return ["i386"]

    @property
    def requirements(self):
        return self.get_requirements()

    @property
    def critical_requirements(self):
        return self.get_requirements(include_optional=False)

    def get_fs_type_for_path(self, path):
        """Return the filesystem type a given path uses"""
        path_drive = system.get_drive_for_path(path)
        for drive in self.get_drives():
            for partition in drive.get("children", []):
                if "/dev/%s" % partition["name"] == path_drive:
                    return partition["fstype"]

    def get_glxinfo(self):
        """Return a GlxInfo instance if the gfxinfo tool is available"""
        if not self.get("glxinfo"):
            return
        _glxinfo = glxinfo.GlxInfo()
        if not hasattr(_glxinfo, "display"):
            logger.warning("Invalid glxinfo received")
            return
        return _glxinfo

    def get_requirements(self, include_optional=True):
        """Return used system requirements"""
        _requirements = self.required_components.copy()
        if include_optional:
            _requirements += self.optional_components
            if drivers.is_amd():
                _requirements.append("RADEON")
        return _requirements

    def get(self, command):
        """Return a system command path if available"""
        return self._cache["COMMANDS"].get(command)

    def get_terminals(self):
        """Return list of installed terminals"""
        return list(self._cache["TERMINALS"].values())

    def get_soundfonts(self):
        """Return path of available soundfonts"""
        return self._cache["SOUNDFONTS"]

    def get_lib_folders(self):
        """Return shared library folders, sorted by most used to least used"""
        lib_folder_counter = Counter(lib.dirname for lib_list in self.shared_libraries.values() for lib in lib_list)
        return [path[0] for path in lib_folder_counter.most_common()]

    def iter_lib_folders(self):
        """Loop over existing 32/64 bit library folders"""
        exported_lib_folders = set()
        for lib_folder in self.get_lib_folders():
            exported_lib_folders.add(lib_folder)
            yield lib_folder
        for lib_paths in self.multiarch_lib_folders:
            if self.arch != "x86_64":
                # On non amd64 setups, only the first element is relevant
                lib_paths = [lib_paths[0]]
            else:
                # Ignore paths where 64-bit path is link to supposed 32-bit path
                if os.path.realpath(lib_paths[0]) == os.path.realpath(lib_paths[1]):
                    continue
            if all(os.path.exists(path) for path in lib_paths):
                if lib_paths[0] not in exported_lib_folders:
                    yield lib_paths[0]
                if len(lib_paths) != 1:
                    if lib_paths[1] not in exported_lib_folders:
                        yield lib_paths[1]

    def get_ldconfig_libs(self):
        """Return a list of available libraries, as returned by `ldconfig -p`."""
        ldconfig = self.get("ldconfig")
        if not ldconfig:
            logger.error("Could not detect ldconfig on this system")
            return []
        output = system.read_process_output([ldconfig, "-p"]).split("\n")
        return [line.strip("\t") for line in output if line.startswith("\t")]

    def get_shared_libraries(self):
        """Loads all available libraries on the system as SharedLibrary instances
        The libraries are stored in a defaultdict keyed by library name.
        """
        shared_libraries = defaultdict(list)
        for lib_line in self.get_ldconfig_libs():
            try:
                lib = SharedLibrary.new_from_ldconfig(lib_line)
            except ValueError:
                logger.error("Invalid ldconfig line: %s", lib_line)
                continue
            if lib.arch not in self.runtime_architectures:
                continue
            shared_libraries[lib.name].append(lib)
        return shared_libraries

    def populate_libraries(self):
        """Populates the LIBRARIES cache with what is found on the system"""
        self._cache["LIBRARIES"] = {}
        for arch in self.runtime_architectures:
            self._cache["LIBRARIES"][arch] = defaultdict(list)
        for req in self.requirements:
            for lib in SYSTEM_COMPONENTS["LIBRARIES"][req]:
                for shared_lib in self.shared_libraries[lib]:
                    self._cache["LIBRARIES"][shared_lib.arch][req].append(lib)

    def populate_sound_fonts(self):
        """Populates the soundfont cache"""
        self._cache["SOUNDFONTS"] = []
        for folder in self.soundfont_folders:
            if not os.path.exists(folder):
                continue
            for soundfont in os.listdir(folder):
                self._cache["SOUNDFONTS"].append(soundfont)

    def get_missing_requirement_libs(self, req):
        """Return a list of sets of missing libraries for each supported architecture"""
        required_libs = set(SYSTEM_COMPONENTS["LIBRARIES"][req])
        return [list(required_libs - set(self._cache["LIBRARIES"][arch][req])) for arch in self.runtime_architectures]

    def get_missing_libs(self):
        """Return a dictionary of missing libraries"""
        return {req: self.get_missing_requirement_libs(req) for req in self.requirements}

    def is_feature_supported(self, feature):
        """Return whether the system has the necessary libs to support a feature"""
        if feature == "ACO":
            try:
                mesa_version = LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version
                return mesa_version >= "19.3"
            except AttributeError:
                return False
        return not self.get_missing_requirement_libs(feature)[0]
critical_requirements property readonly
display_server property readonly

Return the display server used

flatpak_info_path
has_steam property readonly

Return whether Steam is installed locally

is_flatpak property readonly

Check is we are running inside Flatpak sandbox

multiarch_lib_folders
optional_components
recommended_no_file_open
required_components
requirements property readonly
runtime_architectures property readonly

Return the architectures supported on this machine

soundfont_folders
__init__(self) special
Source code in lutris/util/linux.py
def __init__(self):
    for key in ("COMMANDS", "TERMINALS"):
        self._cache[key] = {}
        for command in SYSTEM_COMPONENTS[key]:
            command_path = shutil.which(command)
            if not command_path:
                command_path = self.get_sbin_path(command)
            if command_path:
                self._cache[key][command] = command_path

    # Detect if system is 64bit capable
    self.is_64_bit = sys.maxsize > 2**32
    self.arch = self.get_arch()
    self.shared_libraries = self.get_shared_libraries()
    self.populate_libraries()
    self.populate_sound_fonts()
    self.soft_limit, self.hard_limit = self.get_file_limits()
    self.glxinfo = self.get_glxinfo()
gamemode_available(self)

Return whether gamemode is available

Source code in lutris/util/linux.py
def gamemode_available(self):
    """Return whether gamemode is available"""
    # Current versions of gamemode use gamemoderun
    if system.find_executable("gamemoderun"):
        return True
    # This is for old versions of gamemode only
    if self.is_feature_supported("GAMEMODE"):
        return True
    return False
get(self, command)

Return a system command path if available

Source code in lutris/util/linux.py
def get(self, command):
    """Return a system command path if available"""
    return self._cache["COMMANDS"].get(command)
get_arch() staticmethod

Return the system architecture only if compatible with the supported architectures from the Lutris API

Source code in lutris/util/linux.py
@staticmethod
def get_arch():
    """Return the system architecture only if compatible
    with the supported architectures from the Lutris API
    """
    machine = platform.machine()
    if machine == "x86_64":
        return "x86_64"
    if machine in ("i386", "i686"):
        return "i386"
    if "armv7" in machine:
        return "armv7"
    logger.warning("Unsupported architecture %s", machine)
get_cpus() staticmethod

Parse the output of /proc/cpuinfo

Source code in lutris/util/linux.py
@staticmethod
def get_cpus():
    """Parse the output of /proc/cpuinfo"""
    cpus = [{}]
    cpu_index = 0
    with open("/proc/cpuinfo", encoding='utf-8') as cpuinfo:
        for line in cpuinfo.readlines():
            if not line.strip():
                cpu_index += 1
                cpus.append({})
                continue
            key, value = line.split(":", 1)
            cpus[cpu_index][key.strip()] = value.strip()
    return [cpu for cpu in cpus if cpu]
get_dist_info() staticmethod

Return distribution information

Source code in lutris/util/linux.py
@staticmethod
def get_dist_info():
    """Return distribution information"""
    if linux_distribution:
        return linux_distribution()
    return "unknown"
get_drives() staticmethod

Return a list of drives with their filesystems

Source code in lutris/util/linux.py
@staticmethod
def get_drives():
    """Return a list of drives with their filesystems"""
    lsblk_output = system.read_process_output(["lsblk", "-f", "--json"])
    return [
        drive
        for drive in json.loads(lsblk_output)["blockdevices"]
        if drive["fstype"] != "squashfs"
    ]
get_file_limits() staticmethod
Source code in lutris/util/linux.py
@staticmethod
def get_file_limits():
    return resource.getrlimit(resource.RLIMIT_NOFILE)
get_fs_type_for_path(self, path)

Return the filesystem type a given path uses

Source code in lutris/util/linux.py
def get_fs_type_for_path(self, path):
    """Return the filesystem type a given path uses"""
    path_drive = system.get_drive_for_path(path)
    for drive in self.get_drives():
        for partition in drive.get("children", []):
            if "/dev/%s" % partition["name"] == path_drive:
                return partition["fstype"]
get_glxinfo(self)

Return a GlxInfo instance if the gfxinfo tool is available

Source code in lutris/util/linux.py
def get_glxinfo(self):
    """Return a GlxInfo instance if the gfxinfo tool is available"""
    if not self.get("glxinfo"):
        return
    _glxinfo = glxinfo.GlxInfo()
    if not hasattr(_glxinfo, "display"):
        logger.warning("Invalid glxinfo received")
        return
    return _glxinfo
get_kernel_version() staticmethod

Get kernel info from /proc/version

Source code in lutris/util/linux.py
@staticmethod
def get_kernel_version():
    """Get kernel info from /proc/version"""
    with open("/proc/version", encoding='utf-8') as kernel_info:
        info = kernel_info.readlines()[0]
        version = info.split(" ")[2]
    return version
get_ldconfig_libs(self)

Return a list of available libraries, as returned by ldconfig -p.

Source code in lutris/util/linux.py
def get_ldconfig_libs(self):
    """Return a list of available libraries, as returned by `ldconfig -p`."""
    ldconfig = self.get("ldconfig")
    if not ldconfig:
        logger.error("Could not detect ldconfig on this system")
        return []
    output = system.read_process_output([ldconfig, "-p"]).split("\n")
    return [line.strip("\t") for line in output if line.startswith("\t")]
get_lib_folders(self)

Return shared library folders, sorted by most used to least used

Source code in lutris/util/linux.py
def get_lib_folders(self):
    """Return shared library folders, sorted by most used to least used"""
    lib_folder_counter = Counter(lib.dirname for lib_list in self.shared_libraries.values() for lib in lib_list)
    return [path[0] for path in lib_folder_counter.most_common()]
get_missing_libs(self)

Return a dictionary of missing libraries

Source code in lutris/util/linux.py
def get_missing_libs(self):
    """Return a dictionary of missing libraries"""
    return {req: self.get_missing_requirement_libs(req) for req in self.requirements}
get_missing_requirement_libs(self, req)

Return a list of sets of missing libraries for each supported architecture

Source code in lutris/util/linux.py
def get_missing_requirement_libs(self, req):
    """Return a list of sets of missing libraries for each supported architecture"""
    required_libs = set(SYSTEM_COMPONENTS["LIBRARIES"][req])
    return [list(required_libs - set(self._cache["LIBRARIES"][arch][req])) for arch in self.runtime_architectures]
get_ram_info() staticmethod

Parse the output of /proc/meminfo and return RAM information in kB

Source code in lutris/util/linux.py
@staticmethod
def get_ram_info():
    """Parse the output of /proc/meminfo and return RAM information in kB"""
    mem = {}
    with open("/proc/meminfo", encoding='utf-8') as meminfo:
        for line in meminfo.readlines():
            key, value = line.split(":", 1)
            mem[key.strip()] = value.strip('kB \n')
    return mem
get_requirements(self, include_optional=True)

Return used system requirements

Source code in lutris/util/linux.py
def get_requirements(self, include_optional=True):
    """Return used system requirements"""
    _requirements = self.required_components.copy()
    if include_optional:
        _requirements += self.optional_components
        if drivers.is_amd():
            _requirements.append("RADEON")
    return _requirements
get_sbin_path(command) staticmethod

Some distributions don't put sbin directories in $PATH

Source code in lutris/util/linux.py
@staticmethod
def get_sbin_path(command):
    """Some distributions don't put sbin directories in $PATH"""
    path_candidates = ["/sbin", "/usr/sbin"]
    for candidate in path_candidates:
        command_path = os.path.join(candidate, command)
        if os.path.exists(command_path):
            return command_path
get_shared_libraries(self)

Loads all available libraries on the system as SharedLibrary instances The libraries are stored in a defaultdict keyed by library name.

Source code in lutris/util/linux.py
def get_shared_libraries(self):
    """Loads all available libraries on the system as SharedLibrary instances
    The libraries are stored in a defaultdict keyed by library name.
    """
    shared_libraries = defaultdict(list)
    for lib_line in self.get_ldconfig_libs():
        try:
            lib = SharedLibrary.new_from_ldconfig(lib_line)
        except ValueError:
            logger.error("Invalid ldconfig line: %s", lib_line)
            continue
        if lib.arch not in self.runtime_architectures:
            continue
        shared_libraries[lib.name].append(lib)
    return shared_libraries
get_soundfonts(self)

Return path of available soundfonts

Source code in lutris/util/linux.py
def get_soundfonts(self):
    """Return path of available soundfonts"""
    return self._cache["SOUNDFONTS"]
get_terminals(self)

Return list of installed terminals

Source code in lutris/util/linux.py
def get_terminals(self):
    """Return list of installed terminals"""
    return list(self._cache["TERMINALS"].values())
has_enough_file_descriptors(self)
Source code in lutris/util/linux.py
def has_enough_file_descriptors(self):
    return self.hard_limit >= self.recommended_no_file_open
is_feature_supported(self, feature)

Return whether the system has the necessary libs to support a feature

Source code in lutris/util/linux.py
def is_feature_supported(self, feature):
    """Return whether the system has the necessary libs to support a feature"""
    if feature == "ACO":
        try:
            mesa_version = LINUX_SYSTEM.glxinfo.GLX_MESA_query_renderer.version
            return mesa_version >= "19.3"
        except AttributeError:
            return False
    return not self.get_missing_requirement_libs(feature)[0]
iter_lib_folders(self)

Loop over existing 32/64 bit library folders

Source code in lutris/util/linux.py
def iter_lib_folders(self):
    """Loop over existing 32/64 bit library folders"""
    exported_lib_folders = set()
    for lib_folder in self.get_lib_folders():
        exported_lib_folders.add(lib_folder)
        yield lib_folder
    for lib_paths in self.multiarch_lib_folders:
        if self.arch != "x86_64":
            # On non amd64 setups, only the first element is relevant
            lib_paths = [lib_paths[0]]
        else:
            # Ignore paths where 64-bit path is link to supposed 32-bit path
            if os.path.realpath(lib_paths[0]) == os.path.realpath(lib_paths[1]):
                continue
        if all(os.path.exists(path) for path in lib_paths):
            if lib_paths[0] not in exported_lib_folders:
                yield lib_paths[0]
            if len(lib_paths) != 1:
                if lib_paths[1] not in exported_lib_folders:
                    yield lib_paths[1]
populate_libraries(self)

Populates the LIBRARIES cache with what is found on the system

Source code in lutris/util/linux.py
def populate_libraries(self):
    """Populates the LIBRARIES cache with what is found on the system"""
    self._cache["LIBRARIES"] = {}
    for arch in self.runtime_architectures:
        self._cache["LIBRARIES"][arch] = defaultdict(list)
    for req in self.requirements:
        for lib in SYSTEM_COMPONENTS["LIBRARIES"][req]:
            for shared_lib in self.shared_libraries[lib]:
                self._cache["LIBRARIES"][shared_lib.arch][req].append(lib)
populate_sound_fonts(self)

Populates the soundfont cache

Source code in lutris/util/linux.py
def populate_sound_fonts(self):
    """Populates the soundfont cache"""
    self._cache["SOUNDFONTS"] = []
    for folder in self.soundfont_folders:
        if not os.path.exists(folder):
            continue
        for soundfont in os.listdir(folder):
            self._cache["SOUNDFONTS"].append(soundfont)

SharedLibrary

Representation of a Linux shared library

Source code in lutris/util/linux.py
class SharedLibrary:
    """Representation of a Linux shared library"""

    default_arch = "i386"

    def __init__(self, name, flags, path):
        self.name = name
        self.flags = [flag.strip() for flag in flags.split(",")]
        self.path = path

    @classmethod
    def new_from_ldconfig(cls, ldconfig_line):
        """Create a SharedLibrary instance from an output line from ldconfig"""
        lib_match = re.match(r"^(.*) \((.*)\) => (.*)$", ldconfig_line)
        if not lib_match:
            raise ValueError("Received incorrect value for ldconfig line: %s" % ldconfig_line)
        return cls(lib_match.group(1), lib_match.group(2), lib_match.group(3))

    @property
    def arch(self):
        """Return the architecture for a shared library"""
        detected_arch = ["x86-64", "x32"]
        for arch in detected_arch:
            if arch in self.flags:
                return arch.replace("-", "_")
        return self.default_arch

    @property
    def basename(self):
        """Return the name of the library without an extention"""
        return self.name.split(".so")[0]

    @property
    def dirname(self):
        """Return the directory where the lib resides"""
        return os.path.dirname(self.path)

    def __str__(self):
        return "%s (%s)" % (self.name, self.arch)
arch property readonly

Return the architecture for a shared library

basename property readonly

Return the name of the library without an extention

default_arch
dirname property readonly

Return the directory where the lib resides

__init__(self, name, flags, path) special
Source code in lutris/util/linux.py
def __init__(self, name, flags, path):
    self.name = name
    self.flags = [flag.strip() for flag in flags.split(",")]
    self.path = path
__str__(self) special
Source code in lutris/util/linux.py
def __str__(self):
    return "%s (%s)" % (self.name, self.arch)
new_from_ldconfig(ldconfig_line) classmethod

Create a SharedLibrary instance from an output line from ldconfig

Source code in lutris/util/linux.py
@classmethod
def new_from_ldconfig(cls, ldconfig_line):
    """Create a SharedLibrary instance from an output line from ldconfig"""
    lib_match = re.match(r"^(.*) \((.*)\) => (.*)$", ldconfig_line)
    if not lib_match:
        raise ValueError("Received incorrect value for ldconfig line: %s" % ldconfig_line)
    return cls(lib_match.group(1), lib_match.group(2), lib_match.group(3))

gather_system_info()

Get all system information in a single data structure

Source code in lutris/util/linux.py
def gather_system_info():
    """Get all system information in a single data structure"""
    system_info = {}
    if drivers.is_nvidia():
        system_info["nvidia_driver"] = drivers.get_nvidia_driver_info()
        system_info["nvidia_gpus"] = [drivers.get_nvidia_gpu_info(gpu_id) for gpu_id in drivers.get_nvidia_gpu_ids()]
    system_info["gpus"] = [drivers.get_gpu_info(gpu) for gpu in drivers.get_gpus()]
    system_info["env"] = dict(os.environ)
    system_info["missing_libs"] = LINUX_SYSTEM.get_missing_libs()
    system_info["cpus"] = LINUX_SYSTEM.get_cpus()
    system_info["drives"] = LINUX_SYSTEM.get_drives()
    system_info["ram"] = LINUX_SYSTEM.get_ram_info()
    system_info["dist"] = LINUX_SYSTEM.get_dist_info()
    system_info["arch"] = LINUX_SYSTEM.get_arch()
    system_info["kernel"] = LINUX_SYSTEM.get_kernel_version()
    system_info["glxinfo"] = glxinfo.GlxInfo().as_dict()
    return system_info

gather_system_info_str()

Get all relevant system information already formatted as a string

Source code in lutris/util/linux.py
def gather_system_info_str():
    """Get all relevant system information already formatted as a string"""
    system_info = gather_system_info()
    system_info_readable = {}
    # Add system information
    system_dict = {}
    system_dict["OS"] = ' '.join(system_info["dist"])
    system_dict["Arch"] = system_info["arch"]
    system_dict["Kernel"] = system_info["kernel"]
    system_dict["Desktop"] = system_info["env"].get("XDG_CURRENT_DESKTOP", "Not found")
    system_dict["Display Server"] = system_info["env"].get("XDG_SESSION_TYPE", "Not found")
    system_info_readable["System"] = system_dict
    # Add CPU information
    cpu_dict = {}
    cpu_dict["Vendor"] = system_info["cpus"][0].get("vendor_id", "Vendor unavailable")
    cpu_dict["Model"] = system_info["cpus"][0].get("model name", "Model unavailable")
    cpu_dict["Physical cores"] = system_info["cpus"][0].get("cpu cores", "Physical cores unavailable")
    cpu_dict["Logical cores"] = system_info["cpus"][0].get("siblings", "Logical cores unavailable")
    system_info_readable["CPU"] = cpu_dict
    # Add memory information
    ram_dict = {}
    ram_dict["RAM"] = "%0.1f GB" % (float(system_info["ram"]["MemTotal"]) / 1024 / 1024)
    ram_dict["Swap"] = "%0.1f GB" % (float(system_info["ram"]["SwapTotal"]) / 1024 / 1024)
    system_info_readable["Memory"] = ram_dict
    # Add graphics information
    graphics_dict = {}
    if LINUX_SYSTEM.glxinfo:
        graphics_dict["Vendor"] = system_info["glxinfo"].get("opengl_vendor", "Vendor unavailable")
        graphics_dict["OpenGL Renderer"] = system_info["glxinfo"].get("opengl_renderer", "OpenGL Renderer unavailable")
        graphics_dict["OpenGL Version"] = system_info["glxinfo"].get("opengl_version", "OpenGL Version unavailable")
        graphics_dict["OpenGL Core"] = system_info["glxinfo"].get(
            "opengl_core_profile_version", "OpenGL core unavailable"
        )
        graphics_dict["OpenGL ES"] = system_info["glxinfo"].get("opengl_es_profile_version", "OpenGL ES unavailable")
    else:
        graphics_dict["Vendor"] = "Unable to obtain glxinfo"
    # check Vulkan support
    if vkquery.is_vulkan_supported():
        graphics_dict["Vulkan"] = "Supported"
    else:
        graphics_dict["Vulkan"] = "Not Supported"
    system_info_readable["Graphics"] = graphics_dict

    output = ''
    for section, dictionary in system_info_readable.items():
        output += '[%s]\n' % section
        for key, value in dictionary.items():
            tabs = " " * (16 - len(key))
            output += '%s:%s%s\n' % (key, tabs, value)
        output += '\n'
    return output

get_default_terminal()

Return the default terminal emulator

Source code in lutris/util/linux.py
def get_default_terminal():
    """Return the default terminal emulator"""
    terms = get_terminal_apps()
    if terms:
        return terms[0]
    logger.error("Couldn't find a terminal emulator.")

get_terminal_apps()

Return the list of installed terminal emulators

Source code in lutris/util/linux.py
def get_terminal_apps():
    """Return the list of installed terminal emulators"""
    return LINUX_SYSTEM.get_terminals()

log

Utility module for creating an application wide logger.

CACHE_DIR

DEBUG_FORMATTER

FILE_FORMATTER

LOG_BUFFERS

LOG_FILENAME

SIMPLE_FORMATTER

console_handler

logger

loghandler

magic

magic is a wrapper around the libmagic file identification library.

See https://github.com/ahupp/python-magic for more information.

Usage:

import magic magic.from_file("testdata/test.pdf") 'PDF document, version 1.2' magic.from_file("testdata/test.pdf", mime=True) 'application/pdf' magic.from_buffer(open("testdata/test.pdf").read(1024)) 'PDF document, version 1.2'

MAGIC_CHECK

MAGIC_COMPRESS

MAGIC_CONTINUE

MAGIC_DEBUG

MAGIC_DEVICES

MAGIC_ERROR

MAGIC_EXTENSION

MAGIC_MIME

MAGIC_MIME_ENCODING

MAGIC_MIME_TYPE

MAGIC_NONE

MAGIC_NO_CHECK_APPTYPE

MAGIC_NO_CHECK_ASCII

MAGIC_NO_CHECK_COMPRESS

MAGIC_NO_CHECK_ELF

MAGIC_NO_CHECK_FORTRAN

MAGIC_NO_CHECK_SOFT

MAGIC_NO_CHECK_TAR

MAGIC_NO_CHECK_TOKENS

MAGIC_NO_CHECK_TROFF

MAGIC_PARAM_BYTES_MAX

MAGIC_PARAM_ELF_NOTES_MAX

MAGIC_PARAM_ELF_PHNUM_MAX

MAGIC_PARAM_ELF_SHNUM_MAX

MAGIC_PARAM_INDIR_MAX

MAGIC_PARAM_NAME_MAX

MAGIC_PARAM_REGEX_MAX

MAGIC_PRESERVE_ATIME

MAGIC_RAW

dll

libmagic

magic_check

magic_close

magic_compile

magic_errno

magic_error

magic_open

magic_setflags

magic_t

magic_version

Magic

Magic is a wrapper around the libmagic C library.

Source code in lutris/util/magic.py
class Magic:
    """
    Magic is a wrapper around the libmagic C library.
    """

    def __init__(self, mime=False, magic_file=None, mime_encoding=False,  # pylint: disable=redefined-outer-name
                 keep_going=False, uncompress=False, raw=False, extension=False):
        """
        Create a new libmagic wrapper.

        mime - if True, mimetypes are returned instead of textual descriptions
        mime_encoding - if True, codec is returned
        magic_file - use a mime database other than the system default
        keep_going - don't stop at the first match, keep going
        uncompress - Try to look inside compressed files.
        raw - Do not try to decode "non-printable" chars.
        extension - Print a slash-separated list of valid extensions for the file type found.
        """

        self.cookie = None
        self.flags = MAGIC_NONE
        if mime:
            self.flags |= MAGIC_MIME_TYPE
        if mime_encoding:
            self.flags |= MAGIC_MIME_ENCODING
        if keep_going:
            self.flags |= MAGIC_CONTINUE
        if uncompress:
            self.flags |= MAGIC_COMPRESS
        if raw:
            self.flags |= MAGIC_RAW
        if extension:
            self.flags |= MAGIC_EXTENSION

        self.cookie = magic_open(self.flags)
        self.lock = threading.Lock()

        magic_load(self.cookie, magic_file)

        # MAGIC_EXTENSION was added in 523 or 524, so bail if
        # it doesn't appear to be available
        if extension and (not _has_version or version() < 524):
            raise NotImplementedError('MAGIC_EXTENSION is not supported in this version of libmagic')

        # For https://github.com/ahupp/python-magic/issues/190
        # libmagic has fixed internal limits that some files exceed, causing
        # an error.  We can avoid this (at least for the sample file given)
        # by bumping the limit up.  It's not clear if this is a general solution
        # or whether other internal limits should be increased, but given
        # the lack of other reports I'll assume this is rare.
        if _has_param:
            try:
                self.setparam(MAGIC_PARAM_NAME_MAX, 64)
            except MagicException:
                # some versions of libmagic fail this call,
                # so rather than fail hard just use default behavior
                pass

    def from_buffer(self, buf):
        """
        Identify the contents of `buf`
        """
        with self.lock:
            try:
                # if we're on python3, convert buf to bytes
                # otherwise this string is passed as wchar*
                # which is not what libmagic expects
                if isinstance(buf, str) and str != bytes:
                    buf = buf.encode('utf-8', errors='replace')
                return maybe_decode(magic_buffer(self.cookie, buf))
            except MagicException as e:
                return self._handle509Bug(e)

    def from_file(self, filename):
        # raise FileNotFoundException or IOError if the file does not exist
        with _real_open(filename):
            pass

        with self.lock:
            try:
                return maybe_decode(magic_file(self.cookie, filename))
            except MagicException as e:
                return self._handle509Bug(e)

    def from_descriptor(self, fd):
        with self.lock:
            try:
                return maybe_decode(magic_descriptor(self.cookie, fd))
            except MagicException as e:
                return self._handle509Bug(e)

    def _handle509Bug(self, e):
        # libmagic 5.09 has a bug where it might fail to identify the
        # mimetype of a file and returns null from magic_file (and
        # likely _buffer), but also does not return an error message.
        if e.message is None and (self.flags & MAGIC_MIME_TYPE):
            return "application/octet-stream"
        raise e

    def setparam(self, param, val):
        return magic_setparam(self.cookie, param, val)

    def getparam(self, param):
        return magic_getparam(self.cookie, param)

    def __del__(self):
        # no _thread_check here because there can be no other
        # references to this object at this point.

        # during shutdown magic_close may have been cleared already so
        # make sure it exists before using it.

        # the self.cookie check should be unnecessary and was an
        # incorrect fix for a threading problem, however I'm leaving
        # it in because it's harmless and I'm slightly afraid to
        # remove it.
        if self.cookie and magic_close:
            magic_close(self.cookie)
            self.cookie = None
__del__(self) special
Source code in lutris/util/magic.py
def __del__(self):
    # no _thread_check here because there can be no other
    # references to this object at this point.

    # during shutdown magic_close may have been cleared already so
    # make sure it exists before using it.

    # the self.cookie check should be unnecessary and was an
    # incorrect fix for a threading problem, however I'm leaving
    # it in because it's harmless and I'm slightly afraid to
    # remove it.
    if self.cookie and magic_close:
        magic_close(self.cookie)
        self.cookie = None
__init__(self, mime=False, magic_file=None, mime_encoding=False, keep_going=False, uncompress=False, raw=False, extension=False) special

Create a new libmagic wrapper.

mime - if True, mimetypes are returned instead of textual descriptions mime_encoding - if True, codec is returned magic_file - use a mime database other than the system default keep_going - don't stop at the first match, keep going uncompress - Try to look inside compressed files. raw - Do not try to decode "non-printable" chars. extension - Print a slash-separated list of valid extensions for the file type found.

Source code in lutris/util/magic.py
def __init__(self, mime=False, magic_file=None, mime_encoding=False,  # pylint: disable=redefined-outer-name
             keep_going=False, uncompress=False, raw=False, extension=False):
    """
    Create a new libmagic wrapper.

    mime - if True, mimetypes are returned instead of textual descriptions
    mime_encoding - if True, codec is returned
    magic_file - use a mime database other than the system default
    keep_going - don't stop at the first match, keep going
    uncompress - Try to look inside compressed files.
    raw - Do not try to decode "non-printable" chars.
    extension - Print a slash-separated list of valid extensions for the file type found.
    """

    self.cookie = None
    self.flags = MAGIC_NONE
    if mime:
        self.flags |= MAGIC_MIME_TYPE
    if mime_encoding:
        self.flags |= MAGIC_MIME_ENCODING
    if keep_going:
        self.flags |= MAGIC_CONTINUE
    if uncompress:
        self.flags |= MAGIC_COMPRESS
    if raw:
        self.flags |= MAGIC_RAW
    if extension:
        self.flags |= MAGIC_EXTENSION

    self.cookie = magic_open(self.flags)
    self.lock = threading.Lock()

    magic_load(self.cookie, magic_file)

    # MAGIC_EXTENSION was added in 523 or 524, so bail if
    # it doesn't appear to be available
    if extension and (not _has_version or version() < 524):
        raise NotImplementedError('MAGIC_EXTENSION is not supported in this version of libmagic')

    # For https://github.com/ahupp/python-magic/issues/190
    # libmagic has fixed internal limits that some files exceed, causing
    # an error.  We can avoid this (at least for the sample file given)
    # by bumping the limit up.  It's not clear if this is a general solution
    # or whether other internal limits should be increased, but given
    # the lack of other reports I'll assume this is rare.
    if _has_param:
        try:
            self.setparam(MAGIC_PARAM_NAME_MAX, 64)
        except MagicException:
            # some versions of libmagic fail this call,
            # so rather than fail hard just use default behavior
            pass
from_buffer(self, buf)

Identify the contents of buf

Source code in lutris/util/magic.py
def from_buffer(self, buf):
    """
    Identify the contents of `buf`
    """
    with self.lock:
        try:
            # if we're on python3, convert buf to bytes
            # otherwise this string is passed as wchar*
            # which is not what libmagic expects
            if isinstance(buf, str) and str != bytes:
                buf = buf.encode('utf-8', errors='replace')
            return maybe_decode(magic_buffer(self.cookie, buf))
        except MagicException as e:
            return self._handle509Bug(e)
from_descriptor(self, fd)
Source code in lutris/util/magic.py
def from_descriptor(self, fd):
    with self.lock:
        try:
            return maybe_decode(magic_descriptor(self.cookie, fd))
        except MagicException as e:
            return self._handle509Bug(e)
from_file(self, filename)
Source code in lutris/util/magic.py
def from_file(self, filename):
    # raise FileNotFoundException or IOError if the file does not exist
    with _real_open(filename):
        pass

    with self.lock:
        try:
            return maybe_decode(magic_file(self.cookie, filename))
        except MagicException as e:
            return self._handle509Bug(e)
getparam(self, param)
Source code in lutris/util/magic.py
def getparam(self, param):
    return magic_getparam(self.cookie, param)
setparam(self, param, val)
Source code in lutris/util/magic.py
def setparam(self, param, val):
    return magic_setparam(self.cookie, param, val)

MagicException (Exception)

Source code in lutris/util/magic.py
class MagicException(Exception):
    def __init__(self, message):
        super().__init__(message)
        self.message = message
__init__(self, message) special
Source code in lutris/util/magic.py
def __init__(self, message):
    super().__init__(message)
    self.message = message

coerce_filename(filename)

Source code in lutris/util/magic.py
def coerce_filename(filename):
    if filename is None:
        return None
    # ctypes will implicitly convert unicode strings to bytes with
    # .encode('ascii').  If you use the filesystem encoding
    # then you'll get inconsistent behavior (crashes) depending on the user's
    # LANG environment variable
    if isinstance(filename, str):
        return filename.encode('utf-8', 'surrogateescape')
    return filename

errorcheck_negative_one(result, func, args)

Source code in lutris/util/magic.py
def errorcheck_negative_one(result, func, args):
    if result == -1:
        err = magic_error(args[0])
        raise MagicException(err)
    return result

errorcheck_null(result, func, args)

Source code in lutris/util/magic.py
def errorcheck_null(result, func, args):
    if result is None:
        err = magic_error(args[0])
        raise MagicException(err)
    return result

from_buffer(buffer, mime=False)

Accepts a binary string and returns the detected filetype. Return value is the mimetype if mime=True, otherwise a human readable name.

magic.from_buffer(open("testdata/test.pdf").read(1024)) 'PDF document, version 1.2'

Source code in lutris/util/magic.py
def from_buffer(buffer, mime=False):
    """
    Accepts a binary string and returns the detected filetype.  Return
    value is the mimetype if mime=True, otherwise a human readable
    name.

    >>> magic.from_buffer(open("testdata/test.pdf").read(1024))
    'PDF document, version 1.2'
    """
    m = _get_magic_type(mime)
    return m.from_buffer(buffer)

from_descriptor(fd, mime=False)

Accepts a file descriptor and returns the detected filetype. Return value is the mimetype if mime=True, otherwise a human readable name.

f = open("testdata/test.pdf") magic.from_descriptor(f.fileno()) 'PDF document, version 1.2'

Source code in lutris/util/magic.py
def from_descriptor(fd, mime=False):
    """
    Accepts a file descriptor and returns the detected filetype.  Return
    value is the mimetype if mime=True, otherwise a human readable
    name.

    >>> f = open("testdata/test.pdf")
    >>> magic.from_descriptor(f.fileno())
    'PDF document, version 1.2'
    """
    m = _get_magic_type(mime)
    return m.from_descriptor(fd)

from_file(filename, mime=False)

" Accepts a filename and returns the detected filetype. Return value is the mimetype if mime=True, otherwise a human readable name.

magic.from_file("testdata/test.pdf", mime=True) 'application/pdf'

Source code in lutris/util/magic.py
def from_file(filename, mime=False):
    """"
    Accepts a filename and returns the detected filetype.  Return
    value is the mimetype if mime=True, otherwise a human readable
    name.

    >>> magic.from_file("testdata/test.pdf", mime=True)
    'application/pdf'
    """
    m = _get_magic_type(mime)
    return m.from_file(filename)

magic_buffer(cookie, buf)

Source code in lutris/util/magic.py
def magic_buffer(cookie, buf):
    return _magic_buffer(cookie, buf, len(buf))

magic_descriptor(cookie, fd)

Source code in lutris/util/magic.py
def magic_descriptor(cookie, fd):
    return _magic_descriptor(cookie, fd)

magic_file(cookie, filename)

Source code in lutris/util/magic.py
def magic_file(cookie, filename):
    return _magic_file(cookie, coerce_filename(filename))

magic_getparam(cookie, param)

Source code in lutris/util/magic.py
def magic_getparam(cookie, param):
    if not _has_param:
        raise NotImplementedError("magic_getparam not implemented")
    val = c_size_t()
    _magic_getparam(cookie, param, byref(val))
    return val.value

magic_load(cookie, filename)

Source code in lutris/util/magic.py
def magic_load(cookie, filename):
    return _magic_load(cookie, coerce_filename(filename))

magic_setparam(cookie, param, val)

Source code in lutris/util/magic.py
def magic_setparam(cookie, param, val):
    if not _has_param:
        raise NotImplementedError("magic_setparam not implemented")
    v = c_size_t(val)
    return _magic_setparam(cookie, param, byref(v))

maybe_decode(s)

Source code in lutris/util/magic.py
def maybe_decode(s):
    if str == bytes:
        return s
    # backslashreplace here because sometimes libmagic will return metadata in the charset
    # of the file, which is unknown to us (e.g the title of a Word doc)
    return s.decode('utf-8', 'backslashreplace')

version()

Source code in lutris/util/magic.py
def version():
    if not _has_version:
        raise NotImplementedError("magic_version not implemented")
    return magic_version()

mame special

database

Utility functions for MAME

CACHE_DIR
get_games(xml_path)

Return a list of all games

Source code in lutris/util/mame/database.py
def get_games(xml_path):
    """Return a list of all games"""
    return {
        machine.attrib["name"]: get_machine_info(machine)
        for machine in iter_machines(xml_path, is_game)
    }
get_machine_info(machine)

Return human readable information about a machine node

Source code in lutris/util/mame/database.py
def get_machine_info(machine):
    """Return human readable information about a machine node"""
    return {
        "description": machine.find("description").text,
        "manufacturer": simplify_manufacturer(machine.find("manufacturer").text),
        "year": machine.find("year").text,
        "roms": [rom.attrib for rom in machine.findall("rom")],
        "ports": [port.attrib for port in machine.findall("port")],
        "devices": [
            {
                "info": device.attrib,
                "name": "".join(
                    [instance.attrib["name"] for instance in device.findall("instance")]
                ),
                "briefname": "".join(
                    [
                        instance.attrib["briefname"]
                        for instance in device.findall("instance")
                    ]
                ),
                "extensions": [
                    extension.attrib["name"]
                    for extension in device.findall("extension")
                ],
            }
            for device in machine.findall("device")
        ],
        "input": machine.find("input").attrib,
        "driver": machine.find("driver").attrib,
    }
get_supported_systems(xml_path, force=False)

Return supported systems (computers and consoles) supported. From the full XML list extracted from MAME, filter the systems that are runnable, not clones and have the ability to run software.

Source code in lutris/util/mame/database.py
def get_supported_systems(xml_path, force=False):
    """Return supported systems (computers and consoles) supported.
    From the full XML list extracted from MAME, filter the systems that are
    runnable, not clones and have the ability to run software.
    """
    systems_cache_path = os.path.join(CACHE_DIR, "systems.json")
    if os.path.exists(systems_cache_path) and not force:
        with open(systems_cache_path, "r", encoding='utf-8') as systems_cache_file:
            try:
                systems = json.load(systems_cache_file)
            except json.JSONDecodeError:
                logger.error("Failed to read systems cache %s", systems_cache_path)
                systems = None
        if systems:
            return systems
    systems = {
        machine.attrib["name"]: get_machine_info(machine)
        for machine in iter_machines(xml_path, is_system)
    }
    if not systems:
        return {}
    with open(systems_cache_path, "w", encoding='utf-8') as systems_cache_file:
        json.dump(systems, systems_cache_file, indent=2)
    return systems
has_software_list(machine)

Return True if the machine has an associated software list

Source code in lutris/util/mame/database.py
def has_software_list(machine):
    """Return True if the machine has an associated software list"""
    _has_software_list = False
    for elem in machine:
        if elem.tag == "device_ref" and elem.attrib["name"] == "software_list":
            _has_software_list = True
    return _has_software_list
is_game(machine)

Return True if the given machine game is an original arcade game Clones return False

Source code in lutris/util/mame/database.py
def is_game(machine):
    """Return True if the given machine game is an original arcade game
    Clones return False
    """
    return (
        machine.attrib["isbios"] == "no"
        and machine.attrib["isdevice"] == "no"
        and machine.attrib["runnable"] == "yes"
        and "romof" not in machine.attrib
        # FIXME: Filter by the machines that accept coins, but not like that
        # and "coin" in machine.find("input").attrib
    )
is_system(machine)

Given a machine XML tag, return True if it is a computer, console or handheld.

Source code in lutris/util/mame/database.py
def is_system(machine):
    """Given a machine XML tag, return True if it is a computer, console or
    handheld.
    """
    if (
        machine.attrib.get("runnable") == "no"
        or machine.attrib.get("isdevice") == "yes"
        or machine.attrib.get("isbios") == "yes"
    ):
        return False
    return has_software_list(machine)
iter_machines(xml_path, filter_func=None)

Iterate through machine nodes in the MAME XML

Source code in lutris/util/mame/database.py
def iter_machines(xml_path, filter_func=None):
    """Iterate through machine nodes in the MAME XML"""
    try:
        root = ElementTree.parse(xml_path).getroot()
    except Exception as ex:  # pylint: disable=broad-except
        logger.error("Failed to read MAME XML: %s", ex)
        return []
    for machine in root:
        if filter_func and not filter_func(machine):
            continue
        yield machine
simplify_manufacturer(manufacturer)

Give simplified names for some manufacturers

Source code in lutris/util/mame/database.py
def simplify_manufacturer(manufacturer):
    """Give simplified names for some manufacturers"""
    manufacturer_map = {
        "Amstrad plc": "Amstrad",
        "Apple Computer": "Apple",
        "Commodore Business Machines": "Commodore",
    }
    return manufacturer_map.get(manufacturer, manufacturer)

ini

Manipulate MAME ini files

MameIni

Looks like an ini file and yet it is not one!

Source code in lutris/util/mame/ini.py
class MameIni:

    """Looks like an ini file and yet it is not one!"""

    def __init__(self, ini_path):
        if not path_exists(ini_path):
            raise OSError("File %s does not exist" % ini_path)
        self.ini_path = ini_path
        self.lines = []
        self.config = {}

    def parse(self, line):
        """Store configuration value from a line"""
        line = line.strip()
        if not line or line.startswith("#"):
            return None, None
        key, *_value = line.split(maxsplit=1)
        if _value:
            return key, _value[0]
        return key, None

    def read(self):
        """Reads the content of the ini file"""
        with open(self.ini_path, "r", encoding='utf-8') as ini_file:
            for line in ini_file.readlines():
                self.lines.append(line)
                print(line)
                config_key, config_value = self.parse(line)
                if config_key:
                    self.config[config_key] = config_value

    def write(self):
        """Writes the file to disk"""
        with open(self.ini_path, "w", encoding='utf-8') as ini_file:
            for line in self.lines:
                config_key, _value = self.parse(line)
                if config_key and self.config[config_key]:
                    ini_file.write("%-26s%s\n" % (config_key, self.config[config_key]))
                elif config_key:
                    ini_file.write("%s\n" % config_key)
                else:
                    ini_file.write(line)
__init__(self, ini_path) special
Source code in lutris/util/mame/ini.py
def __init__(self, ini_path):
    if not path_exists(ini_path):
        raise OSError("File %s does not exist" % ini_path)
    self.ini_path = ini_path
    self.lines = []
    self.config = {}
parse(self, line)

Store configuration value from a line

Source code in lutris/util/mame/ini.py
def parse(self, line):
    """Store configuration value from a line"""
    line = line.strip()
    if not line or line.startswith("#"):
        return None, None
    key, *_value = line.split(maxsplit=1)
    if _value:
        return key, _value[0]
    return key, None
read(self)

Reads the content of the ini file

Source code in lutris/util/mame/ini.py
def read(self):
    """Reads the content of the ini file"""
    with open(self.ini_path, "r", encoding='utf-8') as ini_file:
        for line in ini_file.readlines():
            self.lines.append(line)
            print(line)
            config_key, config_value = self.parse(line)
            if config_key:
                self.config[config_key] = config_value
write(self)

Writes the file to disk

Source code in lutris/util/mame/ini.py
def write(self):
    """Writes the file to disk"""
    with open(self.ini_path, "w", encoding='utf-8') as ini_file:
        for line in self.lines:
            config_key, _value = self.parse(line)
            if config_key and self.config[config_key]:
                ini_file.write("%-26s%s\n" % (config_key, self.config[config_key]))
            elif config_key:
                ini_file.write("%s\n" % config_key)
            else:
                ini_file.write(line)

nvidia

Nvidia library detection from Proton

RTLD_DI_LINKMAP

LinkMap (Structure)

from dlinfo(3)

struct link_map { ElfW(Addr) l_addr; / Difference between the address in the ELF file and the address in memory / char l_name; / Absolute pathname where object was found / ElfW(Dyn) l_ld; / Dynamic section of the shared object / struct link_map l_next, l_prev; / Chain of loaded objects / / Plus additional fields private to the implementation / };

Source code in lutris/util/nvidia.py
class LinkMap(Structure):
    """
    from dlinfo(3)

    struct link_map {
        ElfW(Addr) l_addr;  /* Difference between the
                               address in the ELF file and
                               the address in memory */
        char      *l_name;  /* Absolute pathname where
                               object was found */
        ElfW(Dyn) *l_ld;    /* Dynamic section of the
                               shared object */
        struct link_map *l_next, *l_prev;
                            /* Chain of loaded objects */
        /* Plus additional fields private to the implementation */
    };
    """
    _fields_ = [("l_addr", c_void_p), ("l_name", c_char_p), ("l_ld", c_void_p)]

get_nvidia_dll_path()

Return the path to the location of DLL files for use by Wine/Proton from the NVIDIA Linux driver. See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/issues/71 for background on the chosen method of DLL discovery.

Source code in lutris/util/nvidia.py
def get_nvidia_dll_path():
    """Return the path to the location of DLL files for use by Wine/Proton
    from the NVIDIA Linux driver.
    See https://gitlab.steamos.cloud/steamrt/steam-runtime-tools/-/issues/71 for
    background on the chosen method of DLL discovery.
    """
    libglx_path = get_nvidia_glx_path()
    if not libglx_path:
        logger.warning("Unable to locate libGLX_nvidia")
        return
    nvidia_wine_dir = os.path.join(os.path.dirname(libglx_path), "nvidia/wine")
    if os.path.exists(os.path.join(nvidia_wine_dir, "nvngx.dll")):
        return nvidia_wine_dir

get_nvidia_glx_path()

Return the absolute path to the libGLX_nvidia library

Source code in lutris/util/nvidia.py
def get_nvidia_glx_path():
    """Return the absolute path to the libGLX_nvidia library"""
    try:
        libdl = CDLL("libdl.so.2")
    except OSError:
        logger.error("Unable to load libdl.so.2")
        return None

    try:
        libglx_nvidia = CDLL("libGLX_nvidia.so.0")
    except OSError:
        logger.error("Unable to load libGLX_nvidia.so.0")
        return None

    # from dlinfo(3)
    #
    # int dlinfo (void *restrict handle, int request, void *restrict info)
    dlinfo_func = libdl.dlinfo
    dlinfo_func.argtypes = c_void_p, c_int, c_void_p
    dlinfo_func.restype = c_int

    # Allocate a LinkMap object
    glx_nvidia_info_ptr = POINTER(LinkMap)()

    # Run dlinfo(3) on the handle to libGLX_nvidia.so.0, storing results at the
    # address represented by glx_nvidia_info_ptr
    if (
        dlinfo_func(
            libglx_nvidia._handle, RTLD_DI_LINKMAP, addressof(glx_nvidia_info_ptr)
        ) != 0
    ):
        logger.error("Unable to read Nvidia information")
        return None

    # Grab the contents our of our pointer
    glx_nvidia_info = cast(glx_nvidia_info_ptr, POINTER(LinkMap)).contents

    # Decode the path to our library to a str()
    if glx_nvidia_info.l_name is None:
        logger.error("Error reading the Nvidia library path")
        return None
    try:
        libglx_nvidia_path = os.fsdecode(glx_nvidia_info.l_name)
    except UnicodeDecodeError as ex:
        logger.error("Error decoding the Nvidia library path: %s", ex)
        return None

    # Follow any symlinks to the actual file
    return os.path.realpath(libglx_nvidia_path)

process

Class to manipulate a process

IGNORED_PROCESSES

InvalidPid (Exception)

Exception raised when an operation on a non-existent PID is called

Source code in lutris/util/process.py
class InvalidPid(Exception):

    """Exception raised when an operation on a non-existent PID is called"""

Process

Python abstraction a Linux process

Source code in lutris/util/process.py
class Process:

    """Python abstraction a Linux process"""

    def __init__(self, pid):
        try:
            self.pid = int(pid)
            self.error_cache = []
        except ValueError as err:
            raise InvalidPid("'%s' is not a valid pid" % pid) from err

    def __repr__(self):
        return "Process {}".format(self.pid)

    def __str__(self):
        return "{} ({}:{})".format(self.name, self.pid, self.state)

    def _read_content(self, file_path):
        """Return the contents from a file in /proc"""
        try:
            with open(file_path, encoding='utf-8') as proc_file:
                content = proc_file.read()
        except (ProcessLookupError, FileNotFoundError, PermissionError):
            return ""
        return content

    def get_stat(self, parsed=True):
        stat_filename = "/proc/{}/stat".format(self.pid)
        try:
            with open(stat_filename, encoding='utf-8') as stat_file:
                _stat = stat_file.readline()
        except (ProcessLookupError, FileNotFoundError):
            return None
        if parsed:
            return _stat[_stat.rfind(")") + 1:].split()
        return _stat

    def get_thread_ids(self):
        """Return a list of thread ids opened by process."""
        basedir = "/proc/{}/task/".format(self.pid)
        if os.path.isdir(basedir):
            try:
                return os.listdir(basedir)
            except FileNotFoundError:
                return []
        else:
            return []

    def get_children_pids_of_thread(self, tid):
        """Return pids of child processes opened by thread `tid` of process."""
        children_path = "/proc/{}/task/{}/children".format(self.pid, tid)
        try:
            with open(children_path, encoding='utf-8') as children_file:
                children_content = children_file.read()
        except (FileNotFoundError, ProcessLookupError):
            children_content = ""
        return children_content.strip().split()

    @property
    def name(self):
        """Filename of the executable."""
        _stat = self.get_stat(parsed=False)
        if _stat:
            return _stat[_stat.find("(") + 1:_stat.rfind(")")]
        return None

    @property
    def state(self):
        """One character from the string "RSDZTW" where R is running, S is
        sleeping in an interruptible wait, D is waiting in uninterruptible disk
        sleep, Z is zombie, T is traced or stopped (on a signal), and W is
        paging.
        """
        _stat = self.get_stat()
        if _stat:
            return _stat[0]
        return None

    @property
    def cmdline(self):
        """Return command line used to run the process `pid`."""
        cmdline_path = "/proc/{}/cmdline".format(self.pid)
        _cmdline_content = self._read_content(cmdline_path)
        if _cmdline_content:
            return _cmdline_content.replace("\x00", " ").replace("\\", "/")

    @property
    def cwd(self):
        """Return current working dir of process"""
        cwd_path = "/proc/%d/cwd" % int(self.pid)
        return os.readlink(cwd_path)

    @property
    def environ(self):
        """Return the process' environment variables"""
        environ_path = "/proc/{}/environ".format(self.pid)
        _environ_text = self._read_content(environ_path)
        if not _environ_text:
            return {}
        try:
            return dict([line.split("=", 1) for line in _environ_text.split("\x00") if line])
        except ValueError:
            if environ_path not in self.error_cache:
                logger.error("Failed to parse environment variables: %s", _environ_text)
                self.error_cache.append(environ_path)
            return {}

    @property
    def children(self):
        """Return the child processes of this process"""
        _children = []
        for tid in self.get_thread_ids():
            for child_pid in self.get_children_pids_of_thread(tid):
                _children.append(Process(child_pid))
        return _children

    def iter_children(self):
        """Iterator that yields all the children of a process"""
        for child in self.children:
            yield child
            yield from child.iter_children()

    def wait_for_finish(self):
        """Waits until the process finishes
        This only works if self.pid is a child process of Lutris
        """
        try:
            pid, ret_status = os.waitpid(int(self.pid) * -1, 0)
        except OSError as ex:
            logger.error("Failed to get exit status for PID %s", self.pid)
            logger.error(ex)
            return -1
        logger.info("PID %s exited with code %s", pid, ret_status)
        return ret_status
children property readonly

Return the child processes of this process

cmdline property readonly

Return command line used to run the process pid.

cwd property readonly

Return current working dir of process

environ property readonly

Return the process' environment variables

name property readonly

Filename of the executable.

state property readonly

One character from the string "RSDZTW" where R is running, S is sleeping in an interruptible wait, D is waiting in uninterruptible disk sleep, Z is zombie, T is traced or stopped (on a signal), and W is paging.

__init__(self, pid) special
Source code in lutris/util/process.py
def __init__(self, pid):
    try:
        self.pid = int(pid)
        self.error_cache = []
    except ValueError as err:
        raise InvalidPid("'%s' is not a valid pid" % pid) from err
__repr__(self) special
Source code in lutris/util/process.py
def __repr__(self):
    return "Process {}".format(self.pid)
__str__(self) special
Source code in lutris/util/process.py
def __str__(self):
    return "{} ({}:{})".format(self.name, self.pid, self.state)
get_children_pids_of_thread(self, tid)

Return pids of child processes opened by thread tid of process.

Source code in lutris/util/process.py
def get_children_pids_of_thread(self, tid):
    """Return pids of child processes opened by thread `tid` of process."""
    children_path = "/proc/{}/task/{}/children".format(self.pid, tid)
    try:
        with open(children_path, encoding='utf-8') as children_file:
            children_content = children_file.read()
    except (FileNotFoundError, ProcessLookupError):
        children_content = ""
    return children_content.strip().split()
get_stat(self, parsed=True)
Source code in lutris/util/process.py
def get_stat(self, parsed=True):
    stat_filename = "/proc/{}/stat".format(self.pid)
    try:
        with open(stat_filename, encoding='utf-8') as stat_file:
            _stat = stat_file.readline()
    except (ProcessLookupError, FileNotFoundError):
        return None
    if parsed:
        return _stat[_stat.rfind(")") + 1:].split()
    return _stat
get_thread_ids(self)

Return a list of thread ids opened by process.

Source code in lutris/util/process.py
def get_thread_ids(self):
    """Return a list of thread ids opened by process."""
    basedir = "/proc/{}/task/".format(self.pid)
    if os.path.isdir(basedir):
        try:
            return os.listdir(basedir)
        except FileNotFoundError:
            return []
    else:
        return []
iter_children(self)

Iterator that yields all the children of a process

Source code in lutris/util/process.py
def iter_children(self):
    """Iterator that yields all the children of a process"""
    for child in self.children:
        yield child
        yield from child.iter_children()
wait_for_finish(self)

Waits until the process finishes This only works if self.pid is a child process of Lutris

Source code in lutris/util/process.py
def wait_for_finish(self):
    """Waits until the process finishes
    This only works if self.pid is a child process of Lutris
    """
    try:
        pid, ret_status = os.waitpid(int(self.pid) * -1, 0)
    except OSError as ex:
        logger.error("Failed to get exit status for PID %s", self.pid)
        logger.error(ex)
        return -1
    logger.info("PID %s exited with code %s", pid, ret_status)
    return ret_status

process_watcher

Process management

SYSTEM_PROCESSES

ProcessWatcher

Keeps track of child processes of the client

Source code in lutris/util/process_watcher.py
class ProcessWatcher:
    """Keeps track of child processes of the client"""

    def __init__(self, include_processes, exclude_processes):
        """Create a process watcher.
        Params:
            exclude_processes (str or list): list of processes that shouldn't be monitored
            include_processes (str or list): list of process that should be forced to be monitored
        """
        self.unmonitored_processes = (
            self.parse_process_list(exclude_processes) | SYSTEM_PROCESSES
        ) - self.parse_process_list(include_processes)

    @staticmethod
    def parse_process_list(process_list):
        """Parse a process list that may be given as a string"""
        if not process_list:
            return set()
        if isinstance(process_list, str):
            process_list = shlex.split(process_list)
        # process names from /proc only contain 15 characters
        return {p[0:15] for p in process_list}

    @staticmethod
    def iterate_children():
        """Iterates through all children process of the lutris client.
        This is not accurate since not all processes are started by
        lutris but are started by Systemd instead.
        """
        return Process(os.getpid()).iter_children()

    def iterate_processes(self):
        for child in self.iterate_children():
            if child.state == 'Z':
                continue

            if child.name and child.name not in self.unmonitored_processes:
                yield child

    def is_alive(self, message=None):
        """Returns whether at least one watched process exists"""
        if message:
            sys.stdout.write("%s\n" % message)
        return next(self.iterate_processes(), None) is not None
__init__(self, include_processes, exclude_processes) special

Create a process watcher.

Parameters:

Name Type Description Default
exclude_processes str or list

list of processes that shouldn't be monitored

required
include_processes str or list

list of process that should be forced to be monitored

required
Source code in lutris/util/process_watcher.py
def __init__(self, include_processes, exclude_processes):
    """Create a process watcher.
    Params:
        exclude_processes (str or list): list of processes that shouldn't be monitored
        include_processes (str or list): list of process that should be forced to be monitored
    """
    self.unmonitored_processes = (
        self.parse_process_list(exclude_processes) | SYSTEM_PROCESSES
    ) - self.parse_process_list(include_processes)
is_alive(self, message=None)

Returns whether at least one watched process exists

Source code in lutris/util/process_watcher.py
def is_alive(self, message=None):
    """Returns whether at least one watched process exists"""
    if message:
        sys.stdout.write("%s\n" % message)
    return next(self.iterate_processes(), None) is not None
iterate_children() staticmethod

Iterates through all children process of the lutris client. This is not accurate since not all processes are started by lutris but are started by Systemd instead.

Source code in lutris/util/process_watcher.py
@staticmethod
def iterate_children():
    """Iterates through all children process of the lutris client.
    This is not accurate since not all processes are started by
    lutris but are started by Systemd instead.
    """
    return Process(os.getpid()).iter_children()
iterate_processes(self)
Source code in lutris/util/process_watcher.py
def iterate_processes(self):
    for child in self.iterate_children():
        if child.state == 'Z':
            continue

        if child.name and child.name not in self.unmonitored_processes:
            yield child
parse_process_list(process_list) staticmethod

Parse a process list that may be given as a string

Source code in lutris/util/process_watcher.py
@staticmethod
def parse_process_list(process_list):
    """Parse a process list that may be given as a string"""
    if not process_list:
        return set()
    if isinstance(process_list, str):
        process_list = shlex.split(process_list)
    # process names from /proc only contain 15 characters
    return {p[0:15] for p in process_list}

resources

Utility module to handle media resources

get_icon_path(game_slug)

Return the absolute path for a game_slug icon

Source code in lutris/util/resources.py
def get_icon_path(game_slug):
    """Return the absolute path for a game_slug icon"""
    return os.path.join(settings.ICON_PATH, "lutris_%s.png" % game_slug)

settings

SettingsIO

ConfigParser abstraction.

Source code in lutris/util/settings.py
class SettingsIO:

    """ConfigParser abstraction."""

    def __init__(self, config_file):
        self.config_file = config_file
        self.config = configparser.ConfigParser()
        if os.path.exists(self.config_file):
            try:
                self.config.read([self.config_file])
            except configparser.ParsingError as ex:
                logger.error("Failed to readconfig file %s: %s", self.config_file, ex)
            except UnicodeDecodeError as ex:
                logger.error("Some invalid characters are preventing " "the setting file from loading properly: %s", ex)

    def read_setting(self, key, section="lutris", default=""):
        """Read a setting from the config file

        Params:
            key (str): Setting key
            section (str): Optional section, default to 'lutris'
            default (str): Default value to return if setting not present
        """
        try:
            return self.config.get(section, key)
        except (configparser.NoOptionError, configparser.NoSectionError):
            return default

    def write_setting(self, key, value, section="lutris"):
        if not self.config.has_section(section):
            self.config.add_section(section)
        self.config.set(section, key, str(value))

        with open(self.config_file, "w", encoding='utf-8') as config_file:
            self.config.write(config_file)
__init__(self, config_file) special
Source code in lutris/util/settings.py
def __init__(self, config_file):
    self.config_file = config_file
    self.config = configparser.ConfigParser()
    if os.path.exists(self.config_file):
        try:
            self.config.read([self.config_file])
        except configparser.ParsingError as ex:
            logger.error("Failed to readconfig file %s: %s", self.config_file, ex)
        except UnicodeDecodeError as ex:
            logger.error("Some invalid characters are preventing " "the setting file from loading properly: %s", ex)
read_setting(self, key, section='lutris', default='')

Read a setting from the config file

Parameters:

Name Type Description Default
key str

Setting key

required
section str

Optional section, default to 'lutris'

'lutris'
default str

Default value to return if setting not present

''
Source code in lutris/util/settings.py
def read_setting(self, key, section="lutris", default=""):
    """Read a setting from the config file

    Params:
        key (str): Setting key
        section (str): Optional section, default to 'lutris'
        default (str): Default value to return if setting not present
    """
    try:
        return self.config.get(section, key)
    except (configparser.NoOptionError, configparser.NoSectionError):
        return default
write_setting(self, key, value, section='lutris')
Source code in lutris/util/settings.py
def write_setting(self, key, value, section="lutris"):
    if not self.config.has_section(section):
        self.config.add_section(section)
    self.config.set(section, key, str(value))

    with open(self.config_file, "w", encoding='utf-8') as config_file:
        self.config.write(config_file)

shell

Controls execution of programs in separate shells

get_bash_rc_file(cwd, env, aliases=None)

Return a bash prompt configured with pre-defined environment variables and aliases

Source code in lutris/util/shell.py
def get_bash_rc_file(cwd, env, aliases=None):
    """Return a bash prompt configured with pre-defined environment variables and aliases"""
    script_path = os.path.join(settings.CACHE_DIR, "bashrc.sh")
    env["TERM"] = "xterm"
    exported_environment = "\n".join('export %s="%s"' % (key, value) for key, value in env.items())
    aliases = aliases or {}
    alias_commands = "\n".join('alias %s="%s"' % (key, value) for key, value in aliases.items())
    current_bashrc = os.path.expanduser("~/.bashrc")
    with open(script_path, "w", encoding='utf-8') as script_file:
        script_file.write(
            dedent(
                """
            . %s

            %s
            %s
            cd "%s"
            """ % (
                    current_bashrc,
                    exported_environment,
                    alias_commands,
                    cwd,
                )
            )
        )
    return script_path

get_shell_command(cwd, env, aliases=None)

Generates a scripts whichs opens a bash shell configured with given environment variables and aliases.

Source code in lutris/util/shell.py
def get_shell_command(cwd, env, aliases=None):
    """Generates a scripts whichs opens a bash shell configured with given
    environment variables and aliases.
    """
    bashrc_file = get_bash_rc_file(cwd, env, aliases)
    return get_terminal_script(["bash", "--rcfile", bashrc_file], cwd, env)

get_terminal_script(command, cwd, env)

Write command in a script file and run it.

Running it from a file is likely the only way to set env vars only for the command (not for the terminal app). It's also the only reliable way to keep the term open when the game is quit.

Source code in lutris/util/shell.py
def get_terminal_script(command, cwd, env):
    """Write command in a script file and run it.

    Running it from a file is likely the only way to set env vars only
    for the command (not for the terminal app).
    It's also the only reliable way to keep the term open when the
    game is quit.
    """
    script_path = os.path.join(settings.CACHE_DIR, "run_in_term.sh")
    env["TERM"] = "xterm"
    exported_environment = "\n".join('export %s="%s" ' % (key, value) for key, value in env.items())
    command = " ".join(['"%s"' % token for token in command])
    with open(script_path, "w", encoding='utf-8') as script_file:
        script_file.write(
            dedent(
                """#!/bin/sh
            cd "%s"
            %s
            exec %s
            exit $?
            """ % (cwd, exported_environment, command)
            )
        )
        os.chmod(script_path, 0o744)
    return script_path

steam special

appmanifest

Steam appmanifest file handling

APP_STATE_FLAGS
AppManifest

Representation of an AppManifest file from Steam

Source code in lutris/util/steam/appmanifest.py
class AppManifest:
    """Representation of an AppManifest file from Steam"""

    def __init__(self, appmanifest_path):
        self.appmanifest_path = appmanifest_path
        self.steamapps_path, filename = os.path.split(appmanifest_path)
        self.steamid = re.findall(r"(\d+)", filename)[-1]
        self.appmanifest_data = {}

        if path_exists(appmanifest_path):
            with open(appmanifest_path, "r", encoding='utf-8') as appmanifest_file:
                self.appmanifest_data = vdf_parse(appmanifest_file, {})
        else:
            logger.error("Path to AppManifest file %s doesn't exist", appmanifest_path)

    def __repr__(self):
        return "<AppManifest: %s>" % self.appmanifest_path

    @property
    def app_state(self):
        """State of the app (dictionary containing game specific info)"""
        return self.appmanifest_data.get("AppState") or {}

    @property
    def user_config(self):
        """Return the user configuration part"""
        return self.app_state.get("UserConfig") or {}

    @property
    def name(self):
        """Return the game name from either the state or the user config"""
        _name = self.app_state.get("name")
        if not _name:
            _name = self.user_config.get("name")
        return _name

    @property
    def slug(self):
        """Return a slugified version of the name"""
        return slugify(self.name)

    @property
    def installdir(self):
        """Path where the game is installed"""
        return self.app_state.get("installdir")

    @property
    def states(self):
        """Return the states of a Steam game."""
        states = []
        state_flags = self.app_state.get("StateFlags", 0)
        state_flags = bin(int(state_flags))[:1:-1]
        for index, flag in enumerate(state_flags):
            if flag == "1":
                states.append(APP_STATE_FLAGS[index + 1])
        return states

    def is_installed(self):
        """True if the game is fully installed"""
        return "Fully Installed" in self.states

    def get_install_path(self):
        """Absolute path of the installation directory"""
        if not self.installdir:
            return None
        install_path = fix_path_case(os.path.join(self.steamapps_path, "common", self.installdir))
        if install_path and path_exists(install_path):
            return install_path
        return None

    def get_platform(self):
        """Platform the game uses (linux or windows)"""
        steamapps_paths = get_steamapps_paths()
        if self.steamapps_path in steamapps_paths["linux"]:
            return "linux"
        if self.steamapps_path in steamapps_paths["windows"]:
            return "windows"
        raise ValueError("Can't find %s in %s" % (self.steamapps_path, steamapps_paths))
app_state property readonly

State of the app (dictionary containing game specific info)

installdir property readonly

Path where the game is installed

name property readonly

Return the game name from either the state or the user config

slug property readonly

Return a slugified version of the name

states property readonly

Return the states of a Steam game.

user_config property readonly

Return the user configuration part

__init__(self, appmanifest_path) special
Source code in lutris/util/steam/appmanifest.py
def __init__(self, appmanifest_path):
    self.appmanifest_path = appmanifest_path
    self.steamapps_path, filename = os.path.split(appmanifest_path)
    self.steamid = re.findall(r"(\d+)", filename)[-1]
    self.appmanifest_data = {}

    if path_exists(appmanifest_path):
        with open(appmanifest_path, "r", encoding='utf-8') as appmanifest_file:
            self.appmanifest_data = vdf_parse(appmanifest_file, {})
    else:
        logger.error("Path to AppManifest file %s doesn't exist", appmanifest_path)
__repr__(self) special
Source code in lutris/util/steam/appmanifest.py
def __repr__(self):
    return "<AppManifest: %s>" % self.appmanifest_path
get_install_path(self)

Absolute path of the installation directory

Source code in lutris/util/steam/appmanifest.py
def get_install_path(self):
    """Absolute path of the installation directory"""
    if not self.installdir:
        return None
    install_path = fix_path_case(os.path.join(self.steamapps_path, "common", self.installdir))
    if install_path and path_exists(install_path):
        return install_path
    return None
get_platform(self)

Platform the game uses (linux or windows)

Source code in lutris/util/steam/appmanifest.py
def get_platform(self):
    """Platform the game uses (linux or windows)"""
    steamapps_paths = get_steamapps_paths()
    if self.steamapps_path in steamapps_paths["linux"]:
        return "linux"
    if self.steamapps_path in steamapps_paths["windows"]:
        return "windows"
    raise ValueError("Can't find %s in %s" % (self.steamapps_path, steamapps_paths))
is_installed(self)

True if the game is fully installed

Source code in lutris/util/steam/appmanifest.py
def is_installed(self):
    """True if the game is fully installed"""
    return "Fully Installed" in self.states
get_appmanifest_from_appid(steamapps_path, appid)

Given the steam apps path and appid, return the corresponding appmanifest

Source code in lutris/util/steam/appmanifest.py
def get_appmanifest_from_appid(steamapps_path, appid):
    """Given the steam apps path and appid, return the corresponding appmanifest"""
    if not steamapps_path:
        raise ValueError("steamapps_path is mandatory")
    if not path_exists(steamapps_path):
        raise IOError("steamapps_path must be a valid directory")
    if not appid:
        raise ValueError("Missing mandatory appid")
    appmanifest_path = os.path.join(steamapps_path, "appmanifest_%s.acf" % appid)
    if not path_exists(appmanifest_path):
        return None
    return AppManifest(appmanifest_path)
get_appmanifests(steamapps_path)

Return the list for all appmanifest files in a Steam library folder

Source code in lutris/util/steam/appmanifest.py
def get_appmanifests(steamapps_path):
    """Return the list for all appmanifest files in a Steam library folder"""
    return [f for f in os.listdir(steamapps_path) if re.match(r"^appmanifest_\d+.acf$", f)]
get_path_from_appmanifest(steamapps_path, appid)

Return the path where a Steam game is installed.

Source code in lutris/util/steam/appmanifest.py
def get_path_from_appmanifest(steamapps_path, appid):
    """Return the path where a Steam game is installed."""
    appmanifest = get_appmanifest_from_appid(steamapps_path, appid)
    if not appmanifest:
        return None
    return appmanifest.get_install_path()

config

Handle Steam configuration

STEAM_DATA_DIRS
get_config_value(config, key)

Fetch a value from a configuration in a case insensitive way

Source code in lutris/util/steam/config.py
def get_config_value(config, key):
    """Fetch a value from a configuration in a case insensitive way"""
    keymap = {k.lower(): k for k in config.keys()}
    if key not in keymap:
        logger.warning(
            "Config key %s not found in %s", key, ", ".join(list(config.keys()))
        )
        return
    return config[keymap[key.lower()]]
get_default_acf(appid, name)

Return a default configuration usable to create a runnable game in Steam

Source code in lutris/util/steam/config.py
def get_default_acf(appid, name):
    """Return a default configuration usable to
    create a runnable game in Steam"""

    userconfig = OrderedDict()
    userconfig["name"] = name
    userconfig["gameid"] = appid

    appstate = OrderedDict()
    appstate["appID"] = appid
    appstate["Universe"] = "1"
    appstate["StateFlags"] = "1026"
    appstate["installdir"] = name
    appstate["UserConfig"] = userconfig
    return {"AppState": appstate}
get_steam_dir()

Main installation directory for Steam

Source code in lutris/util/steam/config.py
def get_steam_dir():
    """Main installation directory for Steam"""
    steam_dir = search_in_steam_dirs("steamapps")
    if steam_dir:
        return steam_dir[:-len("steamapps")]
get_steam_library(steamid)

Return the list of games owned by a SteamID

Source code in lutris/util/steam/config.py
def get_steam_library(steamid):
    """Return the list of games owned by a SteamID"""
    if not steamid:
        raise ValueError("Missing SteamID")
    steam_games_url = (
        "https://api.steampowered.com/"
        "IPlayerService/GetOwnedGames/v0001/"
        "?key={}&steamid={}&format=json&include_appinfo=1"
        "&include_played_free_games=1".format(
            settings.STEAM_API_KEY, steamid
        )
    )
    response = requests.get(steam_games_url)
    if response.status_code > 400:
        logger.error("Invalid response from steam: %s", response)
        return []
    json_data = response.json()
    response = json_data['response']
    if not response:
        logger.info("No games in response of %s", steam_games_url)
        return []
    if 'games' in response:
        return response['games']
    if 'game_count' in response and response['game_count'] == 0:
        return []
    logger.error("Weird response: %s", json_data)
    return []
get_steamapps_paths()
Source code in lutris/util/steam/config.py
def get_steamapps_paths():
    from lutris.runners import steam  # pylint: disable=import-outside-toplevel
    return steam.steam().get_steamapps_dirs()
get_user_steam_id()

Read user's SteamID from Steam config files

Source code in lutris/util/steam/config.py
def get_user_steam_id():
    """Read user's SteamID from Steam config files"""
    user_config = read_user_config()
    if not user_config or "users" not in user_config:
        return
    last_steam_id = None
    for steam_id in user_config["users"]:
        last_steam_id = steam_id
        if get_config_value(user_config["users"][steam_id], "mostrecent") == "1":
            return steam_id
    return last_steam_id
read_config(steam_data_dir)

Read the Steam configuration and return it as an object

Source code in lutris/util/steam/config.py
def read_config(steam_data_dir):
    """Read the Steam configuration and return it as an object"""

    def get_entry_case_insensitive(config_dict, path):
        for key, _value in config_dict.items():
            if key.lower() == path[0].lower():
                if len(path) <= 1:
                    return config_dict[key]

                return get_entry_case_insensitive(config_dict[key], path[1:])
        raise KeyError(path[0])
    if not steam_data_dir:
        return None
    config_filename = os.path.join(steam_data_dir, "config/config.vdf")
    if not system.path_exists(config_filename):
        return None
    with open(config_filename, "r", encoding='utf-8') as steam_config_file:
        config = vdf_parse(steam_config_file, {})
    try:
        return get_entry_case_insensitive(config, ["InstallConfigStore", "Software", "Valve", "Steam"])
    except KeyError as ex:
        logger.error("Steam config %s is empty: %s", config_filename, ex)
read_library_folders(steam_data_dir)

Read the Steam Library Folders config and return it as an object

Source code in lutris/util/steam/config.py
def read_library_folders(steam_data_dir):
    """Read the Steam Library Folders config and return it as an object"""
    def get_entry_case_insensitive(library_dict, path):
        for key, value in library_dict.items():
            if key.lower() == path[0].lower():
                if len(path) <= 1:
                    return value
                return get_entry_case_insensitive(library_dict[key], path[1:])
            raise KeyError(path[0])
    if not steam_data_dir:
        return None
    library_filename = os.path.join(steam_data_dir, "config/libraryfolders.vdf")
    if not system.path_exists(library_filename):
        return None
    with open(library_filename, "r", encoding='utf-8') as steam_library_file:
        library = vdf_parse(steam_library_file, {})
        # The contentstatsid key is unused and causes problems when looking for library paths.
        library["libraryfolders"].pop("contentstatsid", None)
    try:
        return get_entry_case_insensitive(library, ["libraryfolders"])
    except KeyError as ex:
        logger.error("Steam libraryfolders %s is empty: %s", library_filename, ex)
read_user_config()
Source code in lutris/util/steam/config.py
def read_user_config():
    config_filename = search_in_steam_dirs("config/loginusers.vdf")
    if not system.path_exists(config_filename):
        return None
    with open(config_filename, "r", encoding='utf-8') as steam_config_file:
        config = vdf_parse(steam_config_file, {})
    return config
search_in_steam_dirs(file)

Find the (last) file/dir in all the Steam directories

Source code in lutris/util/steam/config.py
def search_in_steam_dirs(file):
    """Find the (last) file/dir in all the Steam directories"""
    for candidate in STEAM_DATA_DIRS:
        path = system.fix_path_case(
            os.path.join(os.path.expanduser(candidate), file)
        )
        if path and system.path_exists(path):
            return path

log

Steam log handling

get_app_log(steam_data_dir, appid, start_time=None)

Return all log entries related to appid from the latest Steam run.

:param start_time: Time tuple, log entries older than this are dumped.

Source code in lutris/util/steam/log.py
def get_app_log(steam_data_dir, appid, start_time=None):
    """Return all log entries related to appid from the latest Steam run.

    :param start_time: Time tuple, log entries older than this are dumped.
    """
    if start_time:
        start_time = time.strftime("%Y-%m-%d %T", start_time)

    app_log = []
    for line in _get_last_content_log(steam_data_dir):
        if start_time and line[1:20] < start_time:
            continue
        if " %s " % appid in line[22:]:
            app_log.append(line)
    return app_log
get_app_state_log(steam_data_dir, appid, start_time=None)

Return state entries for appid from latest block in content_log.txt.

"Fully Installed, Running" means running. "Fully Installed" means stopped.

:param start_time: Time tuple, log entries older than this are dumped.

Source code in lutris/util/steam/log.py
def get_app_state_log(steam_data_dir, appid, start_time=None):
    """Return state entries for appid from latest block in content_log.txt.

    "Fully Installed, Running" means running.
    "Fully Installed" means stopped.

    :param start_time: Time tuple, log entries older than this are dumped.
    """
    state_log = []
    for line in get_app_log(steam_data_dir, appid, start_time):
        line = line.split(" : ")
        if len(line) == 1:
            continue
        if line[0].endswith("state changed"):
            state_log.append(line[1][:-2])
    return state_log

vdf

Read and write VDF files

to_vdf(dict_data, level=0)

Convert a dictionnary to Steam config file format

Source code in lutris/util/steam/vdf.py
def to_vdf(dict_data, level=0):
    """Convert a dictionnary to Steam config file format"""
    vdf_data = ""
    for key in dict_data:
        value = dict_data[key]
        if isinstance(value, dict):
            vdf_data += '%s"%s"\n' % ("\t" * level, key)
            vdf_data += "%s{\n" % ("\t" * level)
            vdf_data += to_vdf(value, level + 1)
            vdf_data += "%s}\n" % ("\t" * level)
        else:
            vdf_data += '%s"%s"\t\t"%s"\n' % ("\t" * level, key, value)
    return vdf_data
vdf_parse(steam_config_file, config)

Parse a Steam config file and return the contents as a dict.

Source code in lutris/util/steam/vdf.py
def vdf_parse(steam_config_file, config):
    """Parse a Steam config file and return the contents as a dict."""
    line = " "
    while line:
        try:
            line = steam_config_file.readline()
        except UnicodeDecodeError:
            logger.error(
                "Error while reading Steam VDF file %s. Returning %s",
                steam_config_file,
                config,
            )
            return config
        if not line or line.strip() == "}":
            return config
        while not line.strip().endswith('"'):
            nextline = steam_config_file.readline()
            if not nextline:
                break
            line = line[:-1] + nextline

        line_elements = line.strip().split('"')
        if len(line_elements) == 3:
            key = line_elements[1]
            steam_config_file.readline()  # skip '{'
            config[key] = vdf_parse(steam_config_file, {})
        else:
            try:
                config[line_elements[1]] = line_elements[3]
            except IndexError:
                logger.error("Malformed config file: %s", line)
    return config
vdf_write(vdf_path, config)

Write a Steam configuration to a vdf file

Source code in lutris/util/steam/vdf.py
def vdf_write(vdf_path, config):
    """Write a Steam configuration to a vdf file"""
    vdf_data = to_vdf(config)
    with open(vdf_path, "w", encoding='utf-8') as vdf_file:
        vdf_file.write(vdf_data)

watcher

Steam game library watcher

SteamWatcher

Watches a Steam library folder and notify changes

Source code in lutris/util/steam/watcher.py
class SteamWatcher:

    """Watches a Steam library folder and notify changes"""

    def __init__(self, steamapps_paths, callback=None):
        self.monitors = []
        self.callback = callback
        for steam_path in steamapps_paths:
            path = Gio.File.new_for_path(steam_path)
            try:
                monitor = path.monitor_directory(Gio.FileMonitorFlags.NONE)
                logger.debug("Watching Steam folder %s", steam_path)
                monitor.connect("changed", self._on_directory_changed)
                self.monitors.append(monitor)
            except GLib.Error as ex:
                logger.exception(ex)

    def _on_directory_changed(self, _monitor, _file, _other_file, event_type):
        path = _file.get_path()
        if not path.endswith(".acf"):
            return
        self.callback(event_type, path)
__init__(self, steamapps_paths, callback=None) special
Source code in lutris/util/steam/watcher.py
def __init__(self, steamapps_paths, callback=None):
    self.monitors = []
    self.callback = callback
    for steam_path in steamapps_paths:
        path = Gio.File.new_for_path(steam_path)
        try:
            monitor = path.monitor_directory(Gio.FileMonitorFlags.NONE)
            logger.debug("Watching Steam folder %s", steam_path)
            monitor.connect("changed", self._on_directory_changed)
            self.monitors.append(monitor)
        except GLib.Error as ex:
            logger.exception(ex)

strings

String utilities

NO_PLAYTIME

get_formatted_playtime(playtime)

Return a human readable value of the play time

Source code in lutris/util/strings.py
def get_formatted_playtime(playtime):
    """Return a human readable value of the play time"""
    if not playtime:
        return NO_PLAYTIME

    try:
        playtime = float(playtime)
    except ValueError:
        logger.warning("Invalid playtime value '%s'", playtime)
        return NO_PLAYTIME

    hours = math.floor(playtime)
    if hours:
        hours_text = "%d hour%s" % (hours, "s" if hours > 1 else "")
    else:
        hours_text = ""

    minutes = int((playtime - hours) * 60)
    if minutes:
        minutes_text = "%d minute%s" % (minutes, "s" if minutes > 1 else "")
    else:
        minutes_text = ""

    formatted_time = " and ".join([text for text in (hours_text, minutes_text) if text])
    if formatted_time:
        return formatted_time
    if playtime:
        return "Less than a minute"
    return NO_PLAYTIME

gtk_safe(string)

Return a string ready to used in Gtk widgets

Source code in lutris/util/strings.py
def gtk_safe(string):
    """Return a string ready to used in Gtk widgets"""
    if not string:
        string = ""
    string = str(string)
    return string.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

human_size(size)

Shows a size in bytes in a more readable way

Source code in lutris/util/strings.py
def human_size(size):
    """Shows a size in bytes in a more readable way"""
    units = ("bytes", "kB", "MB", "GB", "TB", "PB", "nuh uh", "no way", "BS")
    unit_index = 0
    while size > 1024:
        size = size / 1024
        unit_index += 1
    return "%0.1f %s" % (size, units[unit_index])

lookup_string_in_text(string, text)

Return full line if string found in the multi-line text.

Source code in lutris/util/strings.py
def lookup_string_in_text(string, text):
    """Return full line if string found in the multi-line text."""
    output_lines = text.split("\n")
    for line in output_lines:
        if string in line:
            return line

parse_version(version)

Parse a version string

Return a 3 element tuple containing: - The version number as a list of integers - The prefix (whatever characters before the version number) - The suffix (whatever comes after)

Example:: >>> parse_version("3.6-staging") ([3, 6], '', '-staging')

Returns:

Type Description
tuple

(version number as list, prefix, suffix)

Source code in lutris/util/strings.py
def parse_version(version):
    """Parse a version string

    Return a 3 element tuple containing:
     - The version number as a list of integers
     - The prefix (whatever characters before the version number)
     - The suffix (whatever comes after)

     Example::
        >>> parse_version("3.6-staging")
        ([3, 6], '', '-staging')

    Returns:
        tuple: (version number as list, prefix, suffix)
    """
    version_match = re.search(r"(\d[\d\.]+\d)", version)
    if not version_match:
        return [], "", ""
    version_number = version_match.groups()[0]
    prefix = version[0:version_match.span()[0]]
    suffix = version[version_match.span()[1]:]
    return [int(p) for p in version_number.split(".")], suffix, prefix

slugify(value)

Remove special characters from a string and slugify it.

Normalizes string, converts to lowercase, removes non-alpha characters, and converts spaces to hyphens.

Source code in lutris/util/strings.py
def slugify(value):
    """Remove special characters from a string and slugify it.

    Normalizes string, converts to lowercase, removes non-alpha characters,
    and converts spaces to hyphens.
    """
    _value = str(value)
    # This differs from the Lutris website implementation which uses the Django
    # version of `slugify` and uses the "NFKD" normalization method instead of
    # "NFD". This creates some inconsistencies in titles containing a trademark
    # symbols or some other special characters. The website version of slugify
    # will likely get updated to use the same normalization method.
    _value = unicodedata.normalize("NFD", _value).encode("ascii", "ignore")
    _value = _value.decode("utf-8")
    _value = str(re.sub(r"[^\w\s-]", "", _value)).strip().lower()
    slug = re.sub(r"[-\s]+", "-", _value)
    if not slug:
        # The slug is empty, likely because the string contains only non-latin
        # characters
        slug = str(uuid.uuid5(uuid.NAMESPACE_URL, str(value)))
    return slug

split_arguments(args)

Wrapper around shlex.split that is more tolerant of errors

Source code in lutris/util/strings.py
def split_arguments(args):
    """Wrapper around shlex.split that is more tolerant of errors"""
    if not args:
        # shlex.split seems to hangs when passed the None value
        return []
    return _split_arguments(args)

unpack_dependencies(string)

Parse a string to allow for complex dependencies Works in a similar fashion as Debian dependencies, separate dependencies are comma separated and multiple choices for satisfying a dependency are separated by pipes.

quake-steam | quake-gog, some-quake-mod returns:

[('quake-steam', 'quake-gog'), 'some-quake-mod']

Source code in lutris/util/strings.py
def unpack_dependencies(string):
    """Parse a string to allow for complex dependencies
    Works in a similar fashion as Debian dependencies, separate dependencies
    are comma separated and multiple choices for satisfying a dependency are
    separated by pipes.

    Example: quake-steam | quake-gog, some-quake-mod returns:
        [('quake-steam', 'quake-gog'), 'some-quake-mod']
    """
    if not string:
        return []
    dependencies = [dep.strip() for dep in string.split(",") if dep.strip()]
    for index, dependency in enumerate(dependencies):
        if "|" in dependency:
            dependencies[index] = tuple(option.strip() for option in dependency.split("|") if option.strip())
    return [dependency for dependency in dependencies if dependency]

version_sort(versions, reverse=False)

Source code in lutris/util/strings.py
def version_sort(versions, reverse=False):

    def version_key(version):
        version_list, prefix, suffix = parse_version(version)
        # Normalize the length of sub-versions
        sort_key = version_list + [0] * (10 - len(version_list))
        sort_key.append(prefix)
        sort_key.append(suffix)
        return sort_key

    return sorted(versions, key=version_key, reverse=reverse)

system

System utilities

PROTECTED_HOME_FOLDERS

create_folder(path)

Creates a folder specified by path

Source code in lutris/util/system.py
def create_folder(path):
    """Creates a folder specified by path"""
    if not path:
        return
    path = os.path.expanduser(path)
    os.makedirs(path, exist_ok=True)
    return path

execute(command, env=None, cwd=None, log_errors=False, quiet=False, shell=False, timeout=None)

Execute a system command and return its results.

Parameters:

Name Type Description Default
command list

A list containing an executable and its parameters

required
env dict

Dict of values to add to the current environment

None
cwd str

Working directory

None
log_errors bool

Pipe stderr to stdout (might cause slowdowns)

False
quiet bool

Do not display log messages

False
timeout int

Number of seconds the program is allowed to run, disabled by default

None

Returns:

Type Description
str

stdout output

Source code in lutris/util/system.py
def execute(command, env=None, cwd=None, log_errors=False, quiet=False, shell=False, timeout=None):
    """
        Execute a system command and return its results.

        Params:
            command (list): A list containing an executable and its parameters
            env (dict): Dict of values to add to the current environment
            cwd (str): Working directory
            log_errors (bool): Pipe stderr to stdout (might cause slowdowns)
            quiet (bool): Do not display log messages
            timeout (int): Number of seconds the program is allowed to run, disabled by default

        Returns:
            str: stdout output
    """

    # Check if the executable exists
    if not command:
        logger.error("No executable provided!")
        return ""
    if os.path.isabs(command[0]) and not path_exists(command[0]):
        logger.error("No executable found in %s", command)
        return ""

    if not quiet:
        logger.debug("Executing %s", " ".join([str(i) for i in command]))

    # Set up environment
    existing_env = os.environ.copy()
    if env:
        if not quiet:
            logger.debug(" ".join("{}={}".format(k, v) for k, v in env.items()))
        env = {k: v for k, v in env.items() if v is not None}
        existing_env.update(env)

    # Piping stderr can cause slowness in the programs, use carefully
    # (especially when using regedit with wine)
    try:
        with subprocess.Popen(
            command,
            shell=shell,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE if log_errors else subprocess.DEVNULL,
            env=existing_env,
            cwd=cwd,
            errors="replace"
        ) as command_process:
            stdout, stderr = command_process.communicate(timeout=timeout)
    except (OSError, TypeError) as ex:
        logger.error("Could not run command %s (env: %s): %s", command, env, ex)
        return ""
    except subprocess.TimeoutExpired:
        logger.error("Command %s after %s seconds", command, timeout)
        return ""
    if stderr and log_errors:
        logger.error(stderr)
    return stdout.strip()

find_executable(exec_name)

Return the absolute path of an executable

Source code in lutris/util/system.py
def find_executable(exec_name):
    """Return the absolute path of an executable"""
    if not exec_name:
        return None
    return shutil.which(exec_name)

find_mount_point(path)

Return the mount point a file is located on

Source code in lutris/util/system.py
def find_mount_point(path):
    """Return the mount point a file is located on"""
    path = os.path.abspath(path)
    while not os.path.ismount(path):
        path = os.path.dirname(path)
    return path

fix_path_case(path)

Do a case insensitive check, return the real path with correct case. If the path is not for a real file, this corrects as many components as do exist.

Source code in lutris/util/system.py
def fix_path_case(path):
    """Do a case insensitive check, return the real path with correct case. If the path is
    not for a real file, this corrects as many components as do exist."""
    if not path or os.path.exists(path):
        # If a path isn't provided or it exists as is, return it.
        return path
    parts = path.strip("/").split("/")
    current_path = "/"
    for part in parts:
        parent_path = current_path
        current_path = os.path.join(current_path, part)
        if not os.path.exists(current_path) and os.path.isdir(parent_path):
            try:
                path_contents = os.listdir(parent_path)
            except OSError:
                logger.error("Can't read contents of %s", parent_path)
                path_contents = []
            for filename in path_contents:
                if filename.lower() == part.lower():
                    current_path = os.path.join(parent_path, filename)
                    break

    # Only return the path if we got the same number of elements
    if len(parts) == len(current_path.strip("/").split("/")):
        return current_path

get_disk_size(path)

Return the disk size in bytes of a folder

Source code in lutris/util/system.py
def get_disk_size(path):
    """Return the disk size in bytes of a folder"""
    total_size = 0
    for base, _dirs, files in os.walk(path):
        total_size += sum([
            os.stat(os.path.join(base, f)).st_size
            for f in files
            if os.path.isfile(os.path.join(base, f))
        ])
    return total_size

get_drive_for_path(path)

Return the physical drive a file is located on

Source code in lutris/util/system.py
def get_drive_for_path(path):
    """Return the physical drive a file is located on"""
    return get_mountpoint_drives().get(find_mount_point(path))

get_existing_parent(path)

Return the 1st existing parent for a folder (or itself if the path exists and is a directory). returns None, when none of the parents exists.

Source code in lutris/util/system.py
def get_existing_parent(path):
    """Return the 1st existing parent for a folder (or itself if the path
    exists and is a directory). returns None, when none of the parents exists.
    """
    if path == "":
        return None
    if os.path.exists(path) and not os.path.isfile(path):
        return path
    return get_existing_parent(os.path.dirname(path))

get_file_checksum(filename, hash_type)

Return the checksum of type hash_type for a given filename

Source code in lutris/util/system.py
def get_file_checksum(filename, hash_type):
    """Return the checksum of type `hash_type` for a given filename"""
    hasher = hashlib.new(hash_type)
    with open(filename, "rb") as input_file:
        for chunk in iter(lambda: input_file.read(4096), b""):
            hasher.update(chunk)
    return hasher.hexdigest()

get_md5_hash(filename)

Return the md5 hash of a file.

Source code in lutris/util/system.py
def get_md5_hash(filename):
    """Return the md5 hash of a file."""
    md5 = hashlib.md5()
    try:
        with open(filename, "rb") as _file:
            for chunk in iter(lambda: _file.read(8192), b""):
                md5.update(chunk)
    except IOError:
        logger.warning("Error reading %s", filename)
        return False
    return md5.hexdigest()

get_mounted_discs()

Return a list of mounted discs and ISOs

:rtype: list of Gio.Mount

Source code in lutris/util/system.py
def get_mounted_discs():
    """Return a list of mounted discs and ISOs

    :rtype: list of Gio.Mount
    """
    volumes = Gio.VolumeMonitor.get()
    drives = []

    for mount in volumes.get_mounts():
        if mount.get_volume():
            device = mount.get_volume().get_identifier("unix-device")
            if not device:
                logger.debug("No device for mount %s", mount.get_name())
                continue

            # Device is a disk drive or ISO image
            if "/dev/sr" in device or "/dev/loop" in device:
                drives.append(mount.get_root().get_path())
    return drives

get_mountpoint_drives()

Return a mapping of mount points with their corresponding drives

Source code in lutris/util/system.py
def get_mountpoint_drives():
    """Return a mapping of mount points with their corresponding drives"""
    mounts = read_process_output(["mount", "-v"]).split("\n")
    mount_map = []
    for mount in mounts:
        mount_parts = mount.split()
        if len(mount_parts) < 3:
            continue
        mount_map.append((mount_parts[2], mount_parts[0]))
    return dict(mount_map)

get_pid(program, multiple=False)

Return pid of process.

:param str program: Name of the process. :param bool multiple: If True and multiple instances of the program exist, return all of them; if False only return the first one.

Source code in lutris/util/system.py
def get_pid(program, multiple=False):
    """Return pid of process.

    :param str program: Name of the process.
    :param bool multiple: If True and multiple instances of the program exist,
        return all of them; if False only return the first one.
    """
    pids = execute(["pgrep", program])
    if not pids.strip():
        return
    pids = pids.split()
    if multiple:
        return pids
    return pids[0]

get_pids_using_file(path)

Return a set of pids using file path.

Source code in lutris/util/system.py
def get_pids_using_file(path):
    """Return a set of pids using file `path`."""
    if not os.path.exists(path):
        logger.error("Can't return PIDs using non existing file: %s", path)
        return set()
    fuser_path = find_executable("fuser")
    if not fuser_path:
        logger.warning("fuser not available, please install psmisc")
        return set([])
    fuser_output = execute([fuser_path, path], quiet=True)
    return set(fuser_output.split())

get_running_pid_list()

Return the list of PIDs from processes currently running

Source code in lutris/util/system.py
def get_running_pid_list():
    """Return the list of PIDs from processes currently running"""
    return [int(p) for p in os.listdir("/proc") if p[0].isdigit()]

is_executable(exec_path)

Return whether exec_path is an executable

Source code in lutris/util/system.py
def is_executable(exec_path):
    """Return whether exec_path is an executable"""
    return os.access(exec_path, os.X_OK)

is_removeable(path)

Check if a folder is safe to remove (not system or home, ...)

Source code in lutris/util/system.py
def is_removeable(path):
    """Check if a folder is safe to remove (not system or home, ...)"""
    if not path_exists(path):
        return False

    parts = path.strip("/").split("/")
    if parts[0] in ("usr", "var", "lib", "etc", "boot", "sbin", "bin"):
        # Path is part of the system folders
        return False

    if parts[0] == "home":
        if len(parts) <= 2:
            return False
        if len(parts) == 3 and parts[2] in PROTECTED_HOME_FOLDERS:
            return False
    return True

kill_pid(pid)

Terminate a process referenced by its PID

Source code in lutris/util/system.py
def kill_pid(pid):
    """Terminate a process referenced by its PID"""
    try:
        pid = int(pid)
    except ValueError:
        logger.error("Invalid pid %s")
        return
    logger.info("Killing PID %s", pid)
    try:
        os.kill(pid, signal.SIGKILL)
    except OSError:
        logger.error("Could not kill process %s", pid)

list_unique_folders(folders)

Deduplicate directories with the same Device.Inode

Source code in lutris/util/system.py
def list_unique_folders(folders):
    """Deduplicate directories with the same Device.Inode"""
    unique_dirs = {}
    for folder in folders:
        folder_stat = os.stat(folder)
        identifier = "%s.%s" % (folder_stat.st_dev, folder_stat.st_ino)
        if identifier not in unique_dirs:
            unique_dirs[identifier] = folder
    return unique_dirs.values()

make_executable(exec_path)

Source code in lutris/util/system.py
def make_executable(exec_path):
    file_stats = os.stat(exec_path)
    os.chmod(exec_path, file_stats.st_mode | stat.S_IEXEC)

merge_folders(source, destination)

Merges the content of source to destination

Source code in lutris/util/system.py
def merge_folders(source, destination):
    """Merges the content of source to destination"""
    logger.debug("Merging %s into %s", source, destination)
    # Check if dirs_exist_ok is defined ( Python >= 3.8)
    sig = inspect.signature(shutil.copytree)
    if "dirs_exist_ok" in sig.parameters:
        shutil.copytree(source, destination, symlinks=False, ignore_dangling_symlinks=True, dirs_exist_ok=True)
    else:
        shutil.copytree(source, destination, symlinks=False, ignore_dangling_symlinks=True)

path_exists(path, check_symlinks=False, exclude_empty=False)

Wrapper around system.path_exists that doesn't crash with empty values

Parameters:

Name Type Description Default
path str

File to the file to check

required
check_symlinks bool

If the path is a broken symlink, return False

False
exclude_empty bool

If true, consider 0 bytes files as non existing

False
Source code in lutris/util/system.py
def path_exists(path, check_symlinks=False, exclude_empty=False):
    """Wrapper around system.path_exists that doesn't crash with empty values

    Params:
        path (str): File to the file to check
        check_symlinks (bool): If the path is a broken symlink, return False
        exclude_empty (bool): If true, consider 0 bytes files as non existing
    """
    if not path:
        return False
    if os.path.exists(path):
        if exclude_empty:
            return os.stat(path).st_size > 0
        return True
    if os.path.islink(path):
        logger.warning("%s is a broken link", path)
        return not check_symlinks
    return False

python_identifier(unsafe_string)

Converts a string to something that can be used as a python variable

Source code in lutris/util/system.py
def python_identifier(unsafe_string):
    """Converts a string to something that can be used as a python variable"""
    if not isinstance(unsafe_string, str):
        logger.error("Cannot convert %s to a python identifier", type(unsafe_string))
        return

    def _dashrepl(matchobj):
        return matchobj.group(0).replace("-", "_")

    return re.sub(r"(\${)([\w-]*)(})", _dashrepl, unsafe_string)

read_process_output(command, timeout=5)

Return the output of a command as a string

Source code in lutris/util/system.py
def read_process_output(command, timeout=5):
    """Return the output of a command as a string"""
    try:
        return subprocess.check_output(
            command,
            timeout=timeout,
            encoding="utf-8",
            errors="ignore"
        ).strip()
    except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex:
        logger.error("%s command failed: %s", command, ex)
        return ""

remove_folder(path)

Delete a folder specified by path Returns true if the folder was successfully removed.

Source code in lutris/util/system.py
def remove_folder(path):
    """Delete a folder specified by path
    Returns true if the folder was successfully removed.
    """
    if not os.path.exists(path):
        logger.warning("Non existent path: %s", path)
        return
    logger.debug("Removing folder %s", path)
    if os.path.samefile(os.path.expanduser("~"), path):
        raise RuntimeError("Lutris tried to erase home directory!")
    try:
        shutil.rmtree(path)
    except OSError as ex:
        logger.error("Failed to remove folder %s: %s (Error code %s)", path, ex.strerror, ex.errno)
        return False
    return True

reset_library_preloads()

Remove library preloads from environment

Source code in lutris/util/system.py
def reset_library_preloads():
    """Remove library preloads from environment"""
    for key in ("LD_LIBRARY_PATH", "LD_PRELOAD"):
        if os.environ.get(key):
            try:
                del os.environ[key]
            except OSError:
                logger.error("Failed to delete environment variable %s", key)

reverse_expanduser(path)

Replace '/home/username' with '~' in given path.

Source code in lutris/util/system.py
def reverse_expanduser(path):
    """Replace '/home/username' with '~' in given path."""
    if not path:
        return path
    user_path = os.path.expanduser("~")
    if path.startswith(user_path):
        path = path[len(user_path):].strip("/")
        return "~/" + path
    return path

substitute(string_template, variables)

Expand variables on a string template

Parameters:

Name Type Description Default
string_template str

template with variables preceded by $

required
variables dict

mapping of variable identifier > value

required

Returns:

Type Description
str

String with substituted values

Source code in lutris/util/system.py
def substitute(string_template, variables):
    """Expand variables on a string template

    Args:
        string_template (str): template with variables preceded by $
        variables (dict): mapping of variable identifier > value

    Return:
        str: String with substituted values
    """
    string_template = python_identifier(str(string_template))
    identifiers = variables.keys()

    # We support dashes in identifiers but they are not valid in python
    # identifiers, which is a requirement for the templating engine we use
    # Replace the dashes with underscores in the mapping and template
    variables = dict((k.replace("-", "_"), v) for k, v in variables.items())
    for identifier in identifiers:
        string_template = string_template.replace("${}".format(identifier), "${}".format(identifier.replace("-", "_")))

    template = string.Template(string_template)
    if string_template in list(variables.keys()):
        return variables[string_template]
    return template.safe_substitute(variables)

update_desktop_icons()

Update Icon for GTK+ desktop manager Other desktop manager icon cache commands must be added here if needed

Source code in lutris/util/system.py
def update_desktop_icons():
    """Update Icon for GTK+ desktop manager
    Other desktop manager icon cache commands must be added here if needed
    """
    if find_executable("gtk-update-icon-cache"):
        execute(["gtk-update-icon-cache", "-tf", os.path.join(GLib.get_user_data_dir(), "icons/hicolor")], quiet=True)
        execute(["gtk-update-icon-cache", "-tf", os.path.join(settings.RUNTIME_DIR, "icons/hicolor")], quiet=True)

test_config

setup_test_environment()

Sets up a system to be able to run tests

Source code in lutris/util/test_config.py
def setup_test_environment():
    """Sets up a system to be able to run tests"""
    os.environ["LUTRIS_SKIP_INIT"] = "1"
    schema.syncdb()
    startup.init_lutris()

timer

Timer module

Timer

Simple Timer class to time code

Source code in lutris/util/timer.py
class Timer:

    """Simple Timer class to time code"""

    def __init__(self):
        self._start = None
        self._end = None
        self.finished = False

    def start(self):
        """Starts the timer"""
        self._end = None
        self._start = datetime.datetime.now()
        self.finished = False

    def end(self):
        """Ends the timer"""
        self._end = datetime.datetime.now()
        self.finished = True

    @property
    def duration(self):
        """Return the total duration of the timer"""
        if not self._start:
            return 0

        if not self.finished:
            _duration = (datetime.datetime.now() - self._start).seconds
        else:
            _duration = (self._end - self._start).seconds

        return _duration
duration property readonly

Return the total duration of the timer

__init__(self) special
Source code in lutris/util/timer.py
def __init__(self):
    self._start = None
    self._end = None
    self.finished = False
end(self)

Ends the timer

Source code in lutris/util/timer.py
def end(self):
    """Ends the timer"""
    self._end = datetime.datetime.now()
    self.finished = True
start(self)

Starts the timer

Source code in lutris/util/timer.py
def start(self):
    """Starts the timer"""
    self._end = None
    self._start = datetime.datetime.now()
    self.finished = False

update_cache

Manage a cache file of execution times for updates

DATE_FORMAT

UPDATE_CACHE_PATH

get_last_call(key)

Return the time in second since the last update for 'key' was made

Source code in lutris/util/update_cache.py
def get_last_call(key):
    """Return the time in second since the last update for 'key' was made"""
    date = read_date_from_cache(key)
    if not date:
        return 0
    delta = datetime.now() - date
    return delta.seconds

read_date_from_cache(key)

Return a datetime object from 'key'

Source code in lutris/util/update_cache.py
def read_date_from_cache(key):
    """Return a datetime object from 'key'"""
    cache = _read_cache_content()
    date = cache.get(key)
    if not date:
        return
    date = datetime.strptime(date, DATE_FORMAT)
    return date

write_date_to_cache(key)

Write current time to the cache for 'key'

Source code in lutris/util/update_cache.py
def write_date_to_cache(key):
    """Write current time to the cache for 'key'"""
    cache = _read_cache_content()
    cache[key] = datetime.strftime(datetime.now(), DATE_FORMAT)
    with open(UPDATE_CACHE_PATH, "w", encoding='utf-8') as json_file:
        json.dump(cache, json_file, indent=2)

urlhandler

Unused handler registration but since someone reports problems with URL integration once in a while, it could prove itself useful.

register_url_handler()

Register the lutris: protocol to open with the application.

Source code in lutris/util/urlhandler.py
def register_url_handler():
    """Register the lutris: protocol to open with the application."""
    executable = os.path.abspath(sys.argv[0])
    base_key = "desktop.gnome.url-handlers.lutris"
    schema_directory = "/usr/share/glib-2.0/schemas/"
    schema_source = Gio.SettingsSchemaSource.new_from_directory(schema_directory, None, True)
    schema = schema_source.lookup(base_key, True)
    if schema:
        settings = Gio.Settings.new(base_key)
        settings.set_string("command", executable)
    else:
        logger.warning("Schema not installed, cannot register url-handler")

wine special

cabinstall

CabInstaller

Extract and install contents of cab files

Based on an implementation by tonix64: https://github.com/tonix64/python-installcab

Source code in lutris/util/wine/cabinstall.py
class CabInstaller:
    """Extract and install contents of cab files

    Based on an implementation by tonix64: https://github.com/tonix64/python-installcab
    """

    def __init__(self, prefix, arch=None, wine_path=None):
        self.prefix = prefix
        self.winearch = arch or self.get_wineprefix_arch()
        self.tmpdir = tempfile.mkdtemp()
        self.wine_path = wine_path

        self.register_dlls = False  # Whether to register DLLs, I don't the purpose of that
        self.strip_dlls = False  # When registering, strip the full path

    @staticmethod
    def process_key(key):
        """I have no clue why"""
        return key.strip("\\").replace("HKEY_CLASSES_ROOT", "HKEY_LOCAL_MACHINE\\Software\\Classes")

    @staticmethod
    def get_arch_from_manifest(root):
        registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}assemblyIdentity")
        arch = registry_keys[0].attrib["processorArchitecture"]
        arch_map = {"amd64": "win64", "x86": "win32", "wow64": "wow64"}
        return arch_map[arch]

    def get_winebin(self, arch):
        wine_path = self.wine_path or "wine"
        return wine_path if arch in ("win32", "wow64") else wine_path + "64"

    @staticmethod
    def get_arch_from_dll(dll_path):
        if "x86-64" in read_process_output(["file", dll_path]):
            return "win64"
        return "win32"

    def cleanup(self):
        logger.info("Cleaning up %s", self.tmpdir)
        shutil.rmtree(self.tmpdir)

    def check_dll_arch(self, dll_path):
        return self.get_arch_from_dll(dll_path)

    def replace_variables(self, value, arch):
        if "$(" in value:
            value = value.replace("$(runtime.help)", "C:\\windows\\help")
            value = value.replace("$(runtime.inf)", "C:\\windows\\inf")
            value = value.replace("$(runtime.wbem)", "C:\\windows\\wbem")
            value = value.replace("$(runtime.windows)", "C:\\windows")
            value = value.replace("$(runtime.ProgramFiles)", "C:\\windows\\Program Files")
            value = value.replace("$(runtime.programFiles)", "C:\\windows\\Program Files")
            value = value.replace("$(runtime.programFilesX86)", "C:\\windows\\Program Files (x86)")
            value = value.replace("$(runtime.system32)", "C:\\windows\\%s" % self.get_system32_realdir(arch))
            value = value.replace(
                "$(runtime.drivers)",
                "C:\\windows\\%s\\drivers" % self.get_system32_realdir(arch),
            )
        value = value.replace("\\", "\\\\")
        return value

    def process_value(self, reg_value, arch):
        attrs = reg_value.attrib
        name = attrs["name"]
        value = attrs["value"]
        value_type = attrs["valueType"]
        if not name.strip():
            name = "@"
        else:
            name = '"%s"' % name
        name = self.replace_variables(name, arch)
        if value_type == "REG_BINARY":
            value = re.findall("..", value)
            value = "hex:" + ",".join(value)
        elif value_type == "REG_DWORD":
            value = "dword:%s" % value.replace("0x", "")
        elif value_type == "REG_QWORD":
            value = "qword:%s" % value.replace("0x", "")
        elif value_type == "REG_NONE":
            value = None
        elif value_type == "REG_EXPAND_SZ":
            # not sure if we should replace this ones at this point:
            # caps can vary in the pattern
            value = value.replace("%SystemRoot%", "C:\\windows")
            value = value.replace("%ProgramFiles%", "C:\\windows\\Program Files")
            value = value.replace("%WinDir%", "C:\\windows")
            value = value.replace("%ResourceDir%", "C:\\windows")
            value = value.replace("%Public%", "C:\\users\\Public")
            value = value.replace("%LocalAppData%", "C:\\windows\\Public\\Local Settings\\Application Data")
            value = value.replace("%AllUsersProfile%", "C:\\windows")
            value = value.replace("%UserProfile%", "C:\\windows")
            value = value.replace("%ProgramData%", "C:\\ProgramData")
            value = '"%s"' % value
        elif value_type == "REG_SZ":
            value = '"%s"' % value
        else:
            logger.warning("warning unkown type: %s", value_type)
            value = '"%s"' % value
        if value:
            value = self.replace_variables(value, arch)
            if self.strip_dlls:
                if ".dll" in value:
                    value = value.lower().replace("c:\\\\windows\\\\system32\\\\", "")
                    value = value.lower().replace("c:\\\\windows\\\\syswow64\\\\", "")
        return name, value

    def get_registry_from_manifest(self, file_name):
        out = ""
        root = xml.etree.ElementTree.parse(file_name).getroot()
        arch = self.get_arch_from_manifest(root)
        registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}registryKeys")
        if registry_keys:
            for registry_key in list(registry_keys[0]):
                key = self.process_key(registry_key.attrib["keyName"])
                out += "[%s]\n" % key
                for reg_value in registry_key.findall("{urn:schemas-microsoft-com:asm.v3}registryValue"):
                    name, value = self.process_value(reg_value, arch)
                    if value is not None:
                        out += "%s=%s\n" % (name, value)
                out += "\n"
        return (out, arch)

    def get_wineprefix_arch(self):
        with open(os.path.join(self.prefix, "system.reg"), encoding='utf-8') as reg_file:
            for line in reg_file.readlines():
                if line.startswith("#arch=win32"):
                    return "win32"
                if line.startswith("#arch=win64"):
                    return "win64"
        return "win64"

    def get_system32_realdir(self, arch):
        dest_map = {
            ("win64", "win32"): "Syswow64",
            ("win64", "win64"): "System32",
            ("win64", "wow64"): "System32",
            ("win32", "win32"): "System32",
        }
        return dest_map[(self.winearch, arch)]

    def get_dll_destdir(self, dll_path):
        if self.get_arch_from_dll(dll_path) == "win32" and self.winearch == "win64":
            return os.path.join(self.prefix, "drive_c/windows/syswow64")
        return os.path.join(self.prefix, "drive_c/windows/system32")

    def install_dll(self, dll_path):
        dest_dir = self.get_dll_destdir(dll_path)
        logger.debug("Copying %s to %s", dll_path, dest_dir)
        shutil.copy(dll_path, dest_dir)

        dest_dll_path = os.path.join(dest_dir, os.path.basename(dll_path))
        if not self.register_dlls:
            return
        arch = self.get_arch_from_dll(dest_dll_path)
        subprocess.call([self.get_winebin(arch), "regsvr32", os.path.basename(dest_dll_path)])

    def get_registry_files(self, output_files):
        reg_files = []
        for file_path in output_files:
            if file_path.endswith(".manifest"):
                out = "Windows Registry Editor Version 5.00\n\n"
                outdata, arch = self.get_registry_from_manifest(file_path)
                if outdata:
                    out += outdata
                    with open(os.path.join(self.tmpdir, file_path + ".reg"), "w", encoding='utf-8') as reg_file:
                        reg_file.write(out)
                    reg_files.append((file_path + ".reg", arch))
            if file_path.endswith(".dll"):
                self.install_dll(file_path)
        return reg_files

    def apply_to_registry(self, file_path, arch):
        logger.info("Applying %s to registry", file_path)
        subprocess.call([self.get_winebin(arch), "regedit", os.path.join(self.tmpdir, file_path)])

    def extract_from_cab(self, cabfile, component):
        """Extracts files matching a `component` name from a `cabfile`

        Params:
            cabfile (str): Path to a cabfile to extract from
            component (str): component to extract from the cab file

        Returns:
            list: Files extracted from the cab file
        """
        execute(["cabextract", "-F", "*%s*" % component, "-d", self.tmpdir, cabfile])
        return [os.path.join(r, file) for r, d, f in os.walk(self.tmpdir) for file in f]

    def install(self, cabfile, component):
        """Install `component` from `cabfile`"""
        logger.info("Installing %s from %s", component, cabfile)

        for file_path, arch in self.get_registry_files(self.extract_from_cab(cabfile, component)):
            self.apply_to_registry(file_path, arch)

        self.cleanup()
__init__(self, prefix, arch=None, wine_path=None) special
Source code in lutris/util/wine/cabinstall.py
def __init__(self, prefix, arch=None, wine_path=None):
    self.prefix = prefix
    self.winearch = arch or self.get_wineprefix_arch()
    self.tmpdir = tempfile.mkdtemp()
    self.wine_path = wine_path

    self.register_dlls = False  # Whether to register DLLs, I don't the purpose of that
    self.strip_dlls = False  # When registering, strip the full path
apply_to_registry(self, file_path, arch)
Source code in lutris/util/wine/cabinstall.py
def apply_to_registry(self, file_path, arch):
    logger.info("Applying %s to registry", file_path)
    subprocess.call([self.get_winebin(arch), "regedit", os.path.join(self.tmpdir, file_path)])
check_dll_arch(self, dll_path)
Source code in lutris/util/wine/cabinstall.py
def check_dll_arch(self, dll_path):
    return self.get_arch_from_dll(dll_path)
cleanup(self)
Source code in lutris/util/wine/cabinstall.py
def cleanup(self):
    logger.info("Cleaning up %s", self.tmpdir)
    shutil.rmtree(self.tmpdir)
extract_from_cab(self, cabfile, component)

Extracts files matching a component name from a cabfile

Parameters:

Name Type Description Default
cabfile str

Path to a cabfile to extract from

required
component str

component to extract from the cab file

required

Returns:

Type Description
list

Files extracted from the cab file

Source code in lutris/util/wine/cabinstall.py
def extract_from_cab(self, cabfile, component):
    """Extracts files matching a `component` name from a `cabfile`

    Params:
        cabfile (str): Path to a cabfile to extract from
        component (str): component to extract from the cab file

    Returns:
        list: Files extracted from the cab file
    """
    execute(["cabextract", "-F", "*%s*" % component, "-d", self.tmpdir, cabfile])
    return [os.path.join(r, file) for r, d, f in os.walk(self.tmpdir) for file in f]
get_arch_from_dll(dll_path) staticmethod
Source code in lutris/util/wine/cabinstall.py
@staticmethod
def get_arch_from_dll(dll_path):
    if "x86-64" in read_process_output(["file", dll_path]):
        return "win64"
    return "win32"
get_arch_from_manifest(root) staticmethod
Source code in lutris/util/wine/cabinstall.py
@staticmethod
def get_arch_from_manifest(root):
    registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}assemblyIdentity")
    arch = registry_keys[0].attrib["processorArchitecture"]
    arch_map = {"amd64": "win64", "x86": "win32", "wow64": "wow64"}
    return arch_map[arch]
get_dll_destdir(self, dll_path)
Source code in lutris/util/wine/cabinstall.py
def get_dll_destdir(self, dll_path):
    if self.get_arch_from_dll(dll_path) == "win32" and self.winearch == "win64":
        return os.path.join(self.prefix, "drive_c/windows/syswow64")
    return os.path.join(self.prefix, "drive_c/windows/system32")
get_registry_files(self, output_files)
Source code in lutris/util/wine/cabinstall.py
def get_registry_files(self, output_files):
    reg_files = []
    for file_path in output_files:
        if file_path.endswith(".manifest"):
            out = "Windows Registry Editor Version 5.00\n\n"
            outdata, arch = self.get_registry_from_manifest(file_path)
            if outdata:
                out += outdata
                with open(os.path.join(self.tmpdir, file_path + ".reg"), "w", encoding='utf-8') as reg_file:
                    reg_file.write(out)
                reg_files.append((file_path + ".reg", arch))
        if file_path.endswith(".dll"):
            self.install_dll(file_path)
    return reg_files
get_registry_from_manifest(self, file_name)
Source code in lutris/util/wine/cabinstall.py
def get_registry_from_manifest(self, file_name):
    out = ""
    root = xml.etree.ElementTree.parse(file_name).getroot()
    arch = self.get_arch_from_manifest(root)
    registry_keys = root.findall("{urn:schemas-microsoft-com:asm.v3}registryKeys")
    if registry_keys:
        for registry_key in list(registry_keys[0]):
            key = self.process_key(registry_key.attrib["keyName"])
            out += "[%s]\n" % key
            for reg_value in registry_key.findall("{urn:schemas-microsoft-com:asm.v3}registryValue"):
                name, value = self.process_value(reg_value, arch)
                if value is not None:
                    out += "%s=%s\n" % (name, value)
            out += "\n"
    return (out, arch)
get_system32_realdir(self, arch)
Source code in lutris/util/wine/cabinstall.py
def get_system32_realdir(self, arch):
    dest_map = {
        ("win64", "win32"): "Syswow64",
        ("win64", "win64"): "System32",
        ("win64", "wow64"): "System32",
        ("win32", "win32"): "System32",
    }
    return dest_map[(self.winearch, arch)]
get_winebin(self, arch)
Source code in lutris/util/wine/cabinstall.py
def get_winebin(self, arch):
    wine_path = self.wine_path or "wine"
    return wine_path if arch in ("win32", "wow64") else wine_path + "64"
get_wineprefix_arch(self)
Source code in lutris/util/wine/cabinstall.py
def get_wineprefix_arch(self):
    with open(os.path.join(self.prefix, "system.reg"), encoding='utf-8') as reg_file:
        for line in reg_file.readlines():
            if line.startswith("#arch=win32"):
                return "win32"
            if line.startswith("#arch=win64"):
                return "win64"
    return "win64"
install(self, cabfile, component)

Install component from cabfile

Source code in lutris/util/wine/cabinstall.py
def install(self, cabfile, component):
    """Install `component` from `cabfile`"""
    logger.info("Installing %s from %s", component, cabfile)

    for file_path, arch in self.get_registry_files(self.extract_from_cab(cabfile, component)):
        self.apply_to_registry(file_path, arch)

    self.cleanup()
install_dll(self, dll_path)
Source code in lutris/util/wine/cabinstall.py
def install_dll(self, dll_path):
    dest_dir = self.get_dll_destdir(dll_path)
    logger.debug("Copying %s to %s", dll_path, dest_dir)
    shutil.copy(dll_path, dest_dir)

    dest_dll_path = os.path.join(dest_dir, os.path.basename(dll_path))
    if not self.register_dlls:
        return
    arch = self.get_arch_from_dll(dest_dll_path)
    subprocess.call([self.get_winebin(arch), "regsvr32", os.path.basename(dest_dll_path)])
process_key(key) staticmethod

I have no clue why

Source code in lutris/util/wine/cabinstall.py
@staticmethod
def process_key(key):
    """I have no clue why"""
    return key.strip("\\").replace("HKEY_CLASSES_ROOT", "HKEY_LOCAL_MACHINE\\Software\\Classes")
process_value(self, reg_value, arch)
Source code in lutris/util/wine/cabinstall.py
def process_value(self, reg_value, arch):
    attrs = reg_value.attrib
    name = attrs["name"]
    value = attrs["value"]
    value_type = attrs["valueType"]
    if not name.strip():
        name = "@"
    else:
        name = '"%s"' % name
    name = self.replace_variables(name, arch)
    if value_type == "REG_BINARY":
        value = re.findall("..", value)
        value = "hex:" + ",".join(value)
    elif value_type == "REG_DWORD":
        value = "dword:%s" % value.replace("0x", "")
    elif value_type == "REG_QWORD":
        value = "qword:%s" % value.replace("0x", "")
    elif value_type == "REG_NONE":
        value = None
    elif value_type == "REG_EXPAND_SZ":
        # not sure if we should replace this ones at this point:
        # caps can vary in the pattern
        value = value.replace("%SystemRoot%", "C:\\windows")
        value = value.replace("%ProgramFiles%", "C:\\windows\\Program Files")
        value = value.replace("%WinDir%", "C:\\windows")
        value = value.replace("%ResourceDir%", "C:\\windows")
        value = value.replace("%Public%", "C:\\users\\Public")
        value = value.replace("%LocalAppData%", "C:\\windows\\Public\\Local Settings\\Application Data")
        value = value.replace("%AllUsersProfile%", "C:\\windows")
        value = value.replace("%UserProfile%", "C:\\windows")
        value = value.replace("%ProgramData%", "C:\\ProgramData")
        value = '"%s"' % value
    elif value_type == "REG_SZ":
        value = '"%s"' % value
    else:
        logger.warning("warning unkown type: %s", value_type)
        value = '"%s"' % value
    if value:
        value = self.replace_variables(value, arch)
        if self.strip_dlls:
            if ".dll" in value:
                value = value.lower().replace("c:\\\\windows\\\\system32\\\\", "")
                value = value.lower().replace("c:\\\\windows\\\\syswow64\\\\", "")
    return name, value
replace_variables(self, value, arch)
Source code in lutris/util/wine/cabinstall.py
def replace_variables(self, value, arch):
    if "$(" in value:
        value = value.replace("$(runtime.help)", "C:\\windows\\help")
        value = value.replace("$(runtime.inf)", "C:\\windows\\inf")
        value = value.replace("$(runtime.wbem)", "C:\\windows\\wbem")
        value = value.replace("$(runtime.windows)", "C:\\windows")
        value = value.replace("$(runtime.ProgramFiles)", "C:\\windows\\Program Files")
        value = value.replace("$(runtime.programFiles)", "C:\\windows\\Program Files")
        value = value.replace("$(runtime.programFilesX86)", "C:\\windows\\Program Files (x86)")
        value = value.replace("$(runtime.system32)", "C:\\windows\\%s" % self.get_system32_realdir(arch))
        value = value.replace(
            "$(runtime.drivers)",
            "C:\\windows\\%s\\drivers" % self.get_system32_realdir(arch),
        )
    value = value.replace("\\", "\\\\")
    return value

d3d_extras

D3DExtrasManager (DLLManager)
Source code in lutris/util/wine/d3d_extras.py
class D3DExtrasManager(DLLManager):
    component = "D3D Extras"
    base_dir = os.path.join(RUNTIME_DIR, "d3d_extras")
    versions_path = os.path.join(base_dir, "d3d_extras_versions.json")
    managed_dlls = ("d3dx10_33", "d3dx10_34", "d3dx10_35", "d3dx10_36", "d3dx10_37", "d3dx10_38",
                    "d3dx10_39", "d3dx10_40", "d3dx10_41", "d3dx10_42", "d3dx10_43", "d3dx10",
                    "d3dx11_42", "d3dx11_43", "d3dx9_24", "d3dx9_25", "d3dx9_26", "d3dx9_27",
                    "d3dx9_28", "d3dx9_29", "d3dx9_30", "d3dx9_31", "d3dx9_32", "d3dx9_33",
                    "d3dx9_34", "d3dx9_35", "d3dx9_36", "d3dx9_37", "d3dx9_38", "d3dx9_39",
                    "d3dx9_40", "d3dx9_41", "d3dx9_42", "d3dx9_43", "d3dcompiler_33",
                    "d3dcompiler_34", "d3dcompiler_35", "d3dcompiler_36", "d3dcompiler_37",
                    "d3dcompiler_38", "d3dcompiler_39", "d3dcompiler_40", "d3dcompiler_41",
                    "d3dcompiler_42", "d3dcompiler_43", "d3dcompiler_46", "d3dcompiler_47",)
    releases_url = "https://api.github.com/repos/lutris/d3d_extras/releases"
base_dir
component
managed_dlls
releases_url
versions_path

dgvoodoo2

dgvoodoo2Manager (DLLManager)
Source code in lutris/util/wine/dgvoodoo2.py
class dgvoodoo2Manager(DLLManager):
    component = "dgvoodoo2"
    base_dir = os.path.join(RUNTIME_DIR, "dgvoodoo2")
    versions_path = os.path.join(base_dir, "dgvoodoo2_versions.json")
    managed_dlls = ("d3dimm", "ddraw", "glide", "glide2x", "glide3x", )
    managed_appdata_files = ["dgVoodoo/dgVoodoo.conf"]
    releases_url = "https://api.github.com/repos/lutris/dgvoodoo2/releases"
base_dir
component
managed_appdata_files
managed_dlls
releases_url
versions_path

dll_manager

Injects sets of DLLs into a prefix

DLLManager

Utility class to install dlls to a Wine prefix

Source code in lutris/util/wine/dll_manager.py
class DLLManager:
    """Utility class to install dlls to a Wine prefix"""
    component = NotImplemented
    base_dir = NotImplemented
    managed_dlls = NotImplemented
    managed_appdata_files = []  # most managers have none
    versions_path = NotImplemented
    releases_url = NotImplemented
    archs = {
        32: "x32",
        64: "x64"
    }

    def __init__(self, prefix=None, arch="win64", version=None):
        self.prefix = prefix
        if not os.path.isdir(self.base_dir):
            os.makedirs(self.base_dir)
        self._versions = []
        self._version = version
        self.wine_arch = arch

    @property
    def versions(self):
        """Return available versions"""
        self._versions = self.load_versions()
        if not self._versions:
            self._versions = os.listdir(self.base_dir)
        return self._versions

    @property
    def version(self):
        """Return version (latest known version if not provided)"""
        if self._version:
            return self._version
        if self.versions:
            return self.versions[0]

    @property
    def path(self):
        """Path to local folder containing DLLs"""
        version = self.version
        if not version:
            raise RuntimeError(
                "No path can be generated for %s because no version information is available." % self.component)
        return os.path.join(self.base_dir, version)

    @property
    def version_choices(self):
        _choices = [
            (_("Manual"), "manual"),
        ]
        for version in self.versions:
            _choices.append((version, version))
        return _choices

    def load_versions(self):
        if not system.path_exists(self.versions_path):
            return []
        with open(self.versions_path, "r", encoding='utf-8') as version_file:
            try:
                versions = [v["tag_name"] for v in json.load(version_file)]
            except (KeyError, json.decoder.JSONDecodeError):
                logger.warning(
                    "Invalid versions file %s, deleting so it is downloaded on next start.",
                    self.versions_path
                )
                os.remove(self.versions_path)
                return []
        return versions

    @staticmethod
    def is_managed_dll(dll_path):
        """Check if a given DLL path is provided by the component"""
        return False

    def is_available(self):
        """Return whether component is cached locally"""
        return self.version and system.path_exists(self.path)

    def dll_exists(self, dll_name):
        """Check if the dll is provided by the component
        The DLL might not be available for all architectures so
        only check if one exists for the supported ones
        """
        return any(
            system.path_exists(os.path.join(self.path, arch, dll_name + ".dll"))
            for arch in self.archs.values()
        )

    def get_download_url(self):
        """Fetch the download URL from the JSON version file"""
        with open(self.versions_path, "r", encoding='utf-8') as version_file:
            releases = json.load(version_file)
        for release in releases:
            if release["tag_name"] != self.version:
                continue
            return release["assets"][0]["browser_download_url"]

    def download(self):
        """Download component to the local cache; returns True if successful but False
        if the component could not be downloaded."""
        if self.is_available():
            logger.warning("%s already available at %s", self.component, self.path)

        url = self.get_download_url()
        if not url:
            logger.warning("Could not find a release for %s %s", self.component, self.version)
            return False
        archive_path = os.path.join(self.base_dir, os.path.basename(url))
        logger.info("Downloading %s to %s", url, archive_path)
        download_file(url, archive_path, overwrite=True)
        if not system.path_exists(archive_path) or not os.stat(archive_path).st_size:
            logger.error("Failed to download %s %s", self.component, self.version)
            return False
        logger.info("Extracting %s to %s", archive_path, self.path)
        extract_archive(archive_path, self.path, merge_single=True)
        os.remove(archive_path)
        return True

    def enable_dll(self, system_dir, arch, dll_path):
        """Copies dlls to the appropriate destination"""
        dll = os.path.basename(dll_path)
        if system.path_exists(dll_path):
            wine_dll_path = os.path.join(system_dir, dll)
            if system.path_exists(wine_dll_path):
                if not self.is_managed_dll(wine_dll_path) and not os.path.islink(wine_dll_path):
                    # Backing up original version (may not be needed)
                    shutil.move(wine_dll_path, wine_dll_path + ".orig")
                else:
                    os.remove(wine_dll_path)
            try:
                os.symlink(dll_path, wine_dll_path)
            except OSError:
                logger.error("Failed linking %s to %s", dll_path, wine_dll_path)
        else:
            self.disable_dll(system_dir, arch, dll)

    def disable_dll(self, system_dir, _arch, dll):  # pylint: disable=unused-argument
        """Remove DLL from Wine prefix"""
        wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
        if system.path_exists(wine_dll_path + ".orig"):
            if system.path_exists(wine_dll_path):
                os.remove(wine_dll_path)
            shutil.move(wine_dll_path + ".orig", wine_dll_path)

    def enable_user_file(self, appdata_dir, file_path, source_path):
        if system.path_exists(source_path):
            wine_file_path = os.path.join(appdata_dir, file_path)
            wine_file_dir = os.path.dirname(wine_file_path)
            if system.path_exists(wine_file_path):
                if not os.path.islink(wine_file_path):
                    # Backing up original version (may not be needed)
                    shutil.move(wine_file_path, wine_file_path + ".orig")
                else:
                    os.remove(wine_file_path)

            if not os.path.isdir(wine_file_dir):
                os.makedirs(wine_file_dir)

            try:
                os.symlink(source_path, wine_file_path)
            except OSError:
                logger.error("Failed linking %s to %s", source_path, wine_file_path)
        else:
            self.disable_user_file(appdata_dir, file_path)

    def disable_user_file(self, appdata_dir, file_path):
        wine_file_path = os.path.join(appdata_dir, file_path)
        # We only create a symlink; if it is a real file, it mus tbe user data.
        if system.path_exists(wine_file_path) and os.path.islink(wine_file_path):
            os.remove(wine_file_path)
            if system.path_exists(wine_file_path + ".orig"):
                shutil.move(wine_file_path + ".orig", wine_file_path)

    def _iter_dlls(self):
        windows_path = os.path.join(self.prefix, "drive_c/windows")
        if self.wine_arch == "win64":
            system_dirs = {
                self.archs[64]: os.path.join(windows_path, "system32"),
                self.archs[32]: os.path.join(windows_path, "syswow64"),
            }
        elif self.wine_arch == "win32":
            system_dirs = {self.archs[32]: os.path.join(windows_path, "system32")}

        for arch, system_dir in system_dirs.items():
            for dll in self.managed_dlls:
                yield system_dir, arch, dll

    def _iter_appdata_files(self):
        if self.managed_appdata_files:
            prefix_manager = WinePrefixManager(self.prefix)
            appdata_dir = prefix_manager.appdata_dir
            for file in self.managed_appdata_files:
                filename = os.path.basename(file)
                yield appdata_dir, file, filename

    def enable(self):
        """Enable Dlls for the current prefix"""
        if not self.is_available():
            if not self.download():
                logger.error("%s %s could not be enabled because it is not available locally",
                             self.component, self.version)
                return
        for system_dir, arch, dll in self._iter_dlls():
            dll_path = os.path.join(self.path, arch, "%s.dll" % dll)
            self.enable_dll(system_dir, arch, dll_path)
        for appdata_dir, file, filename in self._iter_appdata_files():
            source_path = os.path.join(self.path, filename)
            self.enable_user_file(appdata_dir, file, source_path)

    def disable(self):
        """Disable DLLs for the current prefix"""
        for system_dir, arch, dll in self._iter_dlls():
            self.disable_dll(system_dir, arch, dll)
        for appdata_dir, file, _filename in self._iter_appdata_files():
            self.disable_user_file(appdata_dir, file)

    def fetch_versions(self):
        """Get releases from GitHub"""
        if not os.path.isdir(self.base_dir):
            os.mkdir(self.base_dir)
        download_file(self.releases_url, self.versions_path, overwrite=True)

    def upgrade(self):
        self.fetch_versions()
        if not self.is_available():
            if self.version:
                logger.info("Downloading %s %s...", self.component, self.version)
                self.download()
            else:
                logger.warning("Unable to download %s because version information was not available.", self.component)
archs
base_dir
component
managed_appdata_files
managed_dlls
path property readonly

Path to local folder containing DLLs

releases_url
version property readonly

Return version (latest known version if not provided)

version_choices property readonly
versions property readonly

Return available versions

versions_path
__init__(self, prefix=None, arch='win64', version=None) special
Source code in lutris/util/wine/dll_manager.py
def __init__(self, prefix=None, arch="win64", version=None):
    self.prefix = prefix
    if not os.path.isdir(self.base_dir):
        os.makedirs(self.base_dir)
    self._versions = []
    self._version = version
    self.wine_arch = arch
disable(self)

Disable DLLs for the current prefix

Source code in lutris/util/wine/dll_manager.py
def disable(self):
    """Disable DLLs for the current prefix"""
    for system_dir, arch, dll in self._iter_dlls():
        self.disable_dll(system_dir, arch, dll)
    for appdata_dir, file, _filename in self._iter_appdata_files():
        self.disable_user_file(appdata_dir, file)
disable_dll(self, system_dir, _arch, dll)

Remove DLL from Wine prefix

Source code in lutris/util/wine/dll_manager.py
def disable_dll(self, system_dir, _arch, dll):  # pylint: disable=unused-argument
    """Remove DLL from Wine prefix"""
    wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
    if system.path_exists(wine_dll_path + ".orig"):
        if system.path_exists(wine_dll_path):
            os.remove(wine_dll_path)
        shutil.move(wine_dll_path + ".orig", wine_dll_path)
disable_user_file(self, appdata_dir, file_path)
Source code in lutris/util/wine/dll_manager.py
def disable_user_file(self, appdata_dir, file_path):
    wine_file_path = os.path.join(appdata_dir, file_path)
    # We only create a symlink; if it is a real file, it mus tbe user data.
    if system.path_exists(wine_file_path) and os.path.islink(wine_file_path):
        os.remove(wine_file_path)
        if system.path_exists(wine_file_path + ".orig"):
            shutil.move(wine_file_path + ".orig", wine_file_path)
dll_exists(self, dll_name)

Check if the dll is provided by the component The DLL might not be available for all architectures so only check if one exists for the supported ones

Source code in lutris/util/wine/dll_manager.py
def dll_exists(self, dll_name):
    """Check if the dll is provided by the component
    The DLL might not be available for all architectures so
    only check if one exists for the supported ones
    """
    return any(
        system.path_exists(os.path.join(self.path, arch, dll_name + ".dll"))
        for arch in self.archs.values()
    )
download(self)

Download component to the local cache; returns True if successful but False if the component could not be downloaded.

Source code in lutris/util/wine/dll_manager.py
def download(self):
    """Download component to the local cache; returns True if successful but False
    if the component could not be downloaded."""
    if self.is_available():
        logger.warning("%s already available at %s", self.component, self.path)

    url = self.get_download_url()
    if not url:
        logger.warning("Could not find a release for %s %s", self.component, self.version)
        return False
    archive_path = os.path.join(self.base_dir, os.path.basename(url))
    logger.info("Downloading %s to %s", url, archive_path)
    download_file(url, archive_path, overwrite=True)
    if not system.path_exists(archive_path) or not os.stat(archive_path).st_size:
        logger.error("Failed to download %s %s", self.component, self.version)
        return False
    logger.info("Extracting %s to %s", archive_path, self.path)
    extract_archive(archive_path, self.path, merge_single=True)
    os.remove(archive_path)
    return True
enable(self)

Enable Dlls for the current prefix

Source code in lutris/util/wine/dll_manager.py
def enable(self):
    """Enable Dlls for the current prefix"""
    if not self.is_available():
        if not self.download():
            logger.error("%s %s could not be enabled because it is not available locally",
                         self.component, self.version)
            return
    for system_dir, arch, dll in self._iter_dlls():
        dll_path = os.path.join(self.path, arch, "%s.dll" % dll)
        self.enable_dll(system_dir, arch, dll_path)
    for appdata_dir, file, filename in self._iter_appdata_files():
        source_path = os.path.join(self.path, filename)
        self.enable_user_file(appdata_dir, file, source_path)
enable_dll(self, system_dir, arch, dll_path)

Copies dlls to the appropriate destination

Source code in lutris/util/wine/dll_manager.py
def enable_dll(self, system_dir, arch, dll_path):
    """Copies dlls to the appropriate destination"""
    dll = os.path.basename(dll_path)
    if system.path_exists(dll_path):
        wine_dll_path = os.path.join(system_dir, dll)
        if system.path_exists(wine_dll_path):
            if not self.is_managed_dll(wine_dll_path) and not os.path.islink(wine_dll_path):
                # Backing up original version (may not be needed)
                shutil.move(wine_dll_path, wine_dll_path + ".orig")
            else:
                os.remove(wine_dll_path)
        try:
            os.symlink(dll_path, wine_dll_path)
        except OSError:
            logger.error("Failed linking %s to %s", dll_path, wine_dll_path)
    else:
        self.disable_dll(system_dir, arch, dll)
enable_user_file(self, appdata_dir, file_path, source_path)
Source code in lutris/util/wine/dll_manager.py
def enable_user_file(self, appdata_dir, file_path, source_path):
    if system.path_exists(source_path):
        wine_file_path = os.path.join(appdata_dir, file_path)
        wine_file_dir = os.path.dirname(wine_file_path)
        if system.path_exists(wine_file_path):
            if not os.path.islink(wine_file_path):
                # Backing up original version (may not be needed)
                shutil.move(wine_file_path, wine_file_path + ".orig")
            else:
                os.remove(wine_file_path)

        if not os.path.isdir(wine_file_dir):
            os.makedirs(wine_file_dir)

        try:
            os.symlink(source_path, wine_file_path)
        except OSError:
            logger.error("Failed linking %s to %s", source_path, wine_file_path)
    else:
        self.disable_user_file(appdata_dir, file_path)
fetch_versions(self)

Get releases from GitHub

Source code in lutris/util/wine/dll_manager.py
def fetch_versions(self):
    """Get releases from GitHub"""
    if not os.path.isdir(self.base_dir):
        os.mkdir(self.base_dir)
    download_file(self.releases_url, self.versions_path, overwrite=True)
get_download_url(self)

Fetch the download URL from the JSON version file

Source code in lutris/util/wine/dll_manager.py
def get_download_url(self):
    """Fetch the download URL from the JSON version file"""
    with open(self.versions_path, "r", encoding='utf-8') as version_file:
        releases = json.load(version_file)
    for release in releases:
        if release["tag_name"] != self.version:
            continue
        return release["assets"][0]["browser_download_url"]
is_available(self)

Return whether component is cached locally

Source code in lutris/util/wine/dll_manager.py
def is_available(self):
    """Return whether component is cached locally"""
    return self.version and system.path_exists(self.path)
is_managed_dll(dll_path) staticmethod

Check if a given DLL path is provided by the component

Source code in lutris/util/wine/dll_manager.py
@staticmethod
def is_managed_dll(dll_path):
    """Check if a given DLL path is provided by the component"""
    return False
load_versions(self)
Source code in lutris/util/wine/dll_manager.py
def load_versions(self):
    if not system.path_exists(self.versions_path):
        return []
    with open(self.versions_path, "r", encoding='utf-8') as version_file:
        try:
            versions = [v["tag_name"] for v in json.load(version_file)]
        except (KeyError, json.decoder.JSONDecodeError):
            logger.warning(
                "Invalid versions file %s, deleting so it is downloaded on next start.",
                self.versions_path
            )
            os.remove(self.versions_path)
            return []
    return versions
upgrade(self)
Source code in lutris/util/wine/dll_manager.py
def upgrade(self):
    self.fetch_versions()
    if not self.is_available():
        if self.version:
            logger.info("Downloading %s %s...", self.component, self.version)
            self.download()
        else:
            logger.warning("Unable to download %s because version information was not available.", self.component)

dxvk

DXVK helper module

DXVKManager (DLLManager)
Source code in lutris/util/wine/dxvk.py
class DXVKManager(DLLManager):
    component = "DXVK"
    base_dir = os.path.join(RUNTIME_DIR, "dxvk")
    versions_path = os.path.join(base_dir, "dxvk_versions.json")
    managed_dlls = ("dxgi", "d3d11", "d3d10core", "d3d9", )
    releases_url = "https://api.github.com/repos/lutris/dxvk/releases"

    @staticmethod
    def is_managed_dll(dll_path):
        """Check if a given DLL path is provided by the component

        Very basic check to see if a dll contains the string "dxvk".
        """
        try:
            with open(dll_path, 'rb') as file:
                prev_block_end = b''
                while True:
                    block = file.read(2 * 1024 * 1024)  # 2 MiB
                    if not block:
                        break
                    if b'dxvk' in prev_block_end + block[:4]:
                        return True
                    if b'dxvk' in block:
                        return True

                    prev_block_end = block[-4:]
        except OSError:
            pass
        return False
base_dir
component
managed_dlls
releases_url
versions_path
is_managed_dll(dll_path) staticmethod

Check if a given DLL path is provided by the component

Very basic check to see if a dll contains the string "dxvk".

Source code in lutris/util/wine/dxvk.py
@staticmethod
def is_managed_dll(dll_path):
    """Check if a given DLL path is provided by the component

    Very basic check to see if a dll contains the string "dxvk".
    """
    try:
        with open(dll_path, 'rb') as file:
            prev_block_end = b''
            while True:
                block = file.read(2 * 1024 * 1024)  # 2 MiB
                if not block:
                    break
                if b'dxvk' in prev_block_end + block[:4]:
                    return True
                if b'dxvk' in block:
                    return True

                prev_block_end = block[-4:]
    except OSError:
        pass
    return False

dxvk_nvapi

DXVKNVAPIManager (DLLManager)
Source code in lutris/util/wine/dxvk_nvapi.py
class DXVKNVAPIManager(DLLManager):
    component = "DXVK-NVAPI"
    base_dir = os.path.join(RUNTIME_DIR, "dxvk-nvapi")
    versions_path = os.path.join(base_dir, "dxvk-nvapi_versions.json")
    managed_dlls = ("nvapi", "nvapi64", "nvml")
    releases_url = "https://api.github.com/repos/lutris/dxvk-nvapi/releases"
    dlss_dlls = ("nvngx", "_nvngx")

    def disable_dll(self, system_dir, _arch, dll):  # pylint: disable=unused-argument
        """Remove DLL from Wine prefix"""
        wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
        if system.path_exists(wine_dll_path):
            os.remove(wine_dll_path)

    def enable(self):
        """Enable Dlls for the current prefix"""
        super().enable()
        dlss_dll_dir = get_nvidia_dll_path()
        if not dlss_dll_dir:
            return

        windows_path = os.path.join(self.prefix, "drive_c/windows")
        system_dir = os.path.join(windows_path, "system32")
        for dll in self.dlss_dlls:
            dll_path = os.path.join(dlss_dll_dir, "%s.dll" % dll)
            self.enable_dll(system_dir, "x64", dll_path)

    def disable(self):
        """Disable DLLs for the current prefix"""
        super().disable()
        windows_path = os.path.join(self.prefix, "drive_c/windows")
        system_dir = os.path.join(windows_path, "system32")
        for dll in self.dlss_dlls:
            self.disable_dll(system_dir, "x64", dll)
base_dir
component
dlss_dlls
managed_dlls
releases_url
versions_path
disable(self)

Disable DLLs for the current prefix

Source code in lutris/util/wine/dxvk_nvapi.py
def disable(self):
    """Disable DLLs for the current prefix"""
    super().disable()
    windows_path = os.path.join(self.prefix, "drive_c/windows")
    system_dir = os.path.join(windows_path, "system32")
    for dll in self.dlss_dlls:
        self.disable_dll(system_dir, "x64", dll)
disable_dll(self, system_dir, _arch, dll)

Remove DLL from Wine prefix

Source code in lutris/util/wine/dxvk_nvapi.py
def disable_dll(self, system_dir, _arch, dll):  # pylint: disable=unused-argument
    """Remove DLL from Wine prefix"""
    wine_dll_path = os.path.join(system_dir, "%s.dll" % dll)
    if system.path_exists(wine_dll_path):
        os.remove(wine_dll_path)
enable(self)

Enable Dlls for the current prefix

Source code in lutris/util/wine/dxvk_nvapi.py
def enable(self):
    """Enable Dlls for the current prefix"""
    super().enable()
    dlss_dll_dir = get_nvidia_dll_path()
    if not dlss_dll_dir:
        return

    windows_path = os.path.join(self.prefix, "drive_c/windows")
    system_dir = os.path.join(windows_path, "system32")
    for dll in self.dlss_dlls:
        dll_path = os.path.join(dlss_dll_dir, "%s.dll" % dll)
        self.enable_dll(system_dir, "x64", dll_path)

fsync

Module for detecting the availability of the Linux futex FUTEX_WAIT_MULTIPLE operation, or the Linux futex2 syscalls.

Either of these is required for fsync to work in Wine. Fsync is an alternative implementation of the Windows synchronization primitives that are used to guard data from being accessed by multiple threads concurrently (which would be A Bad Thing™).

Fsync improves upon the previous implementation in Wine of these primitives, known as esync, which in turn improved upon the original implementation known as "Server-side synchronization".

The original implementation used a wineserver call for each synchronization operation, which required multiple context switches per operation.

Esync instead used file descriptors for synchronization, which can be passed around between processes and therefore allowed synchronization to happen directly between the processes involved, instead of going through the wineserver. This made the synchronization operations faster and improved performance of games a bit. A problem with this implementation was that each created synchronization object required one file descriptor, and there is only a limited amount of these available for each process. Some games would run out of available file descriptors, and would stop working. This has been partly mitigated by raising the per-process file descriptor limit, but there are also games that leak synchronization objects continuously while running, and would eventually run out despite the raised limits.

Fsync improved on esync by not requiring a file descriptor for each created synchronization object, and instead using the Linux kernel's futex interface for synchronizations. This matches Windows's implementation more closely and mitigated all the file descriptor related issues of esync. However, since the default futex interface was insufficient for implementing all required synchronization operations, a patch to the Linux kernel was needed, which usually meant that users needed to compile their own Linux kernel with the patch, or install a kernel provided by a third-party. It was attempted to get the kernel patch into the mainline Linux kernel, but it didn't get accepted.

Instead, patches were written that would add a new set of system calls which extend the original futex system calls, dubbed "futex2", and the Wine fsync code was adjusted to make use of these new system calls. The new Wine fsync code is backwards-compatible with the old futex patch, therefore it makes sense for now to detect the presence of either patch in the running kernel. The detection of the old patch can probably be removed when the new patch is merged and in a stable Linux release.

This module's code is based on https://gist.github.com/openglfreak/715d5ab5902497378f1996061dbbf8ec

__all__ special
futex_waitv (Structure)

Linux kernel compatible futex_waitv type.

Fields

val: The expected value. uaddr: The address to wait for. flags: The type and size of the futex.

Source code in lutris/util/wine/fsync.py
class futex_waitv(ctypes.Structure):
    """Linux kernel compatible futex_waitv type.

    Fields:
        val: The expected value.
        uaddr: The address to wait for.
        flags: The type and size of the futex.
    """
    __slots__ = ()
    _fields_ = [
        ("val", ctypes.c_uint64),
        ("uaddr", ctypes.c_void_p),
        ("flags", ctypes.c_uint),
    ]
__slots__ special
timespec (Structure)

Linux kernel compatible timespec type.

Fields

tv_sec: The whole seconds of the timespec. tv_nsec: The nanoseconds of the timespec.

Source code in lutris/util/wine/fsync.py
class timespec(ctypes.Structure):
    """Linux kernel compatible timespec type.

    Fields:
        tv_sec: The whole seconds of the timespec.
        tv_nsec: The nanoseconds of the timespec.
    """
    __slots__ = ()
    _fields_ = [
        ("tv_sec", ctypes.c_long),
        ("tv_nsec", ctypes.c_long),
    ]
__slots__ special
is_fsync_supported()

Checks whether the FUTEX_WAIT_MULTIPLE operation, the futex2 syscalls, or the futex_waitv syscall is supported on this kernel.

Returns:

Type Description

The result of the check.

Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_fsync_supported():
    """Checks whether the FUTEX_WAIT_MULTIPLE operation, the futex2
    syscalls, or the futex_waitv syscall is supported on this kernel.

    Returns:
        The result of the check.
    """
    if is_futex_waitv_supported():
        return True
    if is_futex2_supported():
        return True
    if is_futex_wait_multiple_supported():
        return True
    return False
is_futex2_supported()

Checks whether the Linux futex2 syscall is supported on this kernel.

Returns:

Type Description

Whether this kernel supports the futex2 syscall.

Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_futex2_supported():
    """Checks whether the Linux futex2 syscall is supported on this
    kernel.

    Returns:
        Whether this kernel supports the futex2 syscall.
    """
    try:
        for filename in ("wait", "waitv", "wake"):
            with open("/sys/kernel/futex2/" + filename, "rb") as file:
                if not file.readline().strip().isdigit():
                    return False
    except OSError:
        return False
    return True
is_futex_wait_multiple_supported()

Checks whether the Linux futex FUTEX_WAIT_MULTIPLE operation is supported on this kernel.

Returns:

Type Description

Whether this kernel supports the FUTEX_WAIT_MULTIPLE operation.

Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_futex_wait_multiple_supported():
    """Checks whether the Linux futex FUTEX_WAIT_MULTIPLE operation is
    supported on this kernel.

    Returns:
        Whether this kernel supports the FUTEX_WAIT_MULTIPLE operation.
    """
    try:
        return _get_futex_wait_multiple_op(_get_futex_syscall()) is not None
    except (AttributeError, RuntimeError):
        return False
is_futex_waitv_supported()

Checks whether the Linux 5.16 futex_waitv syscall is supported on this kernel.

Returns:

Type Description

Whether this kernel supports the futex_waitv syscall.

Source code in lutris/util/wine/fsync.py
@functools.lru_cache(None)
def is_futex_waitv_supported():
    """Checks whether the Linux 5.16 futex_waitv syscall is supported on
    this kernel.

    Returns:
        Whether this kernel supports the futex_waitv syscall.
    """
    try:
        ret = _get_futex_waitv_syscall()(None, 0, 0, None)
        return ret[1] != errno.ENOSYS
    except (AttributeError, RuntimeError):
        return False

prefix

Wine prefix management

DEFAULT_DESKTOP_FOLDERS
DEFAULT_DLL_OVERRIDES
DESKTOP_KEYS
DESKTOP_XDG
WinePrefixManager

Class to allow modification of Wine prefixes without the use of Wine

Source code in lutris/util/wine/prefix.py
class WinePrefixManager:
    """Class to allow modification of Wine prefixes without the use of Wine"""

    hkcu_prefix = "HKEY_CURRENT_USER"
    hklm_prefix = "HKEY_LOCAL_MACHINE"

    def __init__(self, path):
        if not path:
            logger.warning("No path specified for Wine prefix")
        self.path = path

    @property
    def user_dir(self):
        """Returns the directory that contains the current user's profile in the WINE prefix."""
        user = os.getenv("USER") or 'lutrisuser'
        return os.path.join(self.path, "drive_c/users/", user)

    @property
    def appdata_dir(self):
        """Returns the app-data directory for the user; this depends on a registry key."""
        user_dir = self.user_dir
        folder = self.get_registry_key(
            self.hkcu_prefix + "/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders",
            "AppData",
        )

        # Don't try to resolve the WIndows path we get- there's
        # just two options, the Vista-and later option and the
        # XP-and-earlier option.
        if folder.lower().endswith("\\application data"):
            return os.path.join(user_dir, "Application Data")  # Windows XP
        return os.path.join(user_dir, "AppData/Roaming")  # Vista

    def setup_defaults(self):
        """Sets the defaults for newly created prefixes"""
        for dll, value in DEFAULT_DLL_OVERRIDES.items():
            self.override_dll(dll, value)
        try:
            self.desktop_integration()
        except OSError as ex:
            logger.error("Failed to setup desktop integration, the prefix may not be valid.")
            logger.exception(ex)

    def get_registry_path(self, key):
        """Matches registry keys to a registry file

        Currently, only HKEY_CURRENT_USER keys are supported.
        """
        if key.startswith(self.hkcu_prefix):
            return os.path.join(self.path, "user.reg")
        if key.startswith(self.hklm_prefix):
            return os.path.join(self.path, "system.reg")
        raise ValueError("Unsupported key '{}'".format(key))

    def get_key_path(self, key):
        for prefix in (self.hkcu_prefix, self.hklm_prefix):
            if key.startswith(prefix):
                return key[len(prefix) + 1:]
        raise ValueError("The key {} is currently not supported by WinePrefixManager".format(key))

    def get_registry_key(self, key, subkey):
        registry = WineRegistry(self.get_registry_path(key))
        return registry.query(self.get_key_path(key), subkey)

    def set_registry_key(self, key, subkey, value):
        registry = WineRegistry(self.get_registry_path(key))
        registry.set_value(self.get_key_path(key), subkey, value)
        registry.save()

    def clear_registry_key(self, key):
        registry = WineRegistry(self.get_registry_path(key))
        registry.clear_key(self.get_key_path(key))
        registry.save()

    def clear_registry_subkeys(self, key, subkeys):
        registry = WineRegistry(self.get_registry_path(key))
        registry.clear_subkeys(self.get_key_path(key), subkeys)
        registry.save()

    def override_dll(self, dll, mode):
        key = self.hkcu_prefix + "/Software/Wine/DllOverrides"
        if mode.startswith("dis"):
            mode = ""
        if mode not in ("builtin", "native", "builtin,native", "native,builtin", ""):
            logger.error("DLL override '%s' mode is not valid", mode)
            return
        self.set_registry_key(key, dll, mode)

    def get_desktop_folders(self):
        """Return the list of desktop folder names loaded from the Windows registry"""
        desktop_folders = []
        for key in DESKTOP_KEYS:
            folder = self.get_registry_key(
                self.hkcu_prefix + "/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders",
                key,
            )
            if not folder:
                logger.warning("Couldn't load shell folder name for %s", key)
                continue
            desktop_folders.append(folder[folder.rfind("\\") + 1:])
        return desktop_folders or DEFAULT_DESKTOP_FOLDERS

    def desktop_integration(self, desktop_dir=None, restore=False):  # noqa: C901
        """Overwrite desktop integration"""
        # pylint: disable=too-many-branches
        # TODO: reduce complexity (18)
        user_dir = self.user_dir
        desktop_folders = self.get_desktop_folders()
        desktop_dir = os.path.expanduser(desktop_dir) if desktop_dir else user_dir

        if system.path_exists(user_dir):
            # Replace or restore desktop integration symlinks
            for i, item in enumerate(desktop_folders):
                path = os.path.join(user_dir, item)
                old_path = path + ".winecfg"

                if os.path.islink(path):
                    if not restore:
                        os.unlink(path)
                elif os.path.isdir(path):
                    try:
                        os.rmdir(path)
                    # We can't delete nonempty dir, so we rename as wine do.
                    except OSError:
                        os.rename(path, old_path)

                # if we want to create a symlink and one is already there, just
                # skip to the next item.  this also makes sure the elif doesn't
                # find a dir (isdir only looks at the target of the symlink).
                if restore and os.path.islink(path):
                    continue
                if restore and not os.path.isdir(path):
                    src_path = get_xdg_entry(DESKTOP_XDG[i])
                    if not src_path:
                        logger.error("No XDG entry found for %s, launcher not created", DESKTOP_XDG[i])
                    else:
                        os.symlink(src_path, path)
                    # We don't need all the others process of the loop
                    continue

                if desktop_dir != user_dir:
                    try:
                        src_path = os.path.join(desktop_dir, item)
                    except TypeError as ex:
                        # There is supposedly a None value in there
                        # The current code shouldn't allow that
                        # Just raise a exception with the values
                        raise RuntimeError("Missing value desktop_dir=%s or item=%s" % (desktop_dir, item)) from ex

                    os.makedirs(src_path, exist_ok=True)
                    os.symlink(src_path, path)
                else:
                    # We use first the renamed dir, otherwise we make it.
                    if os.path.isdir(old_path):
                        os.rename(old_path, path)
                    else:
                        os.makedirs(path, exist_ok=True)

    def set_crash_dialogs(self, enabled):
        """Enable or diable Wine crash dialogs"""
        self.set_registry_key(
            self.hkcu_prefix + "/Software/Wine/WineDbg",
            "ShowCrashDialog",
            1 if enabled else 0,
        )

    def set_virtual_desktop(self, enabled):
        """Enable or disable wine virtual desktop.
        The Lutris virtual desktop is refered to as 'WineDesktop', in Wine the
        virtual desktop name is 'default'.
        """
        path = self.hkcu_prefix + "/Software/Wine/Explorer"
        if enabled:
            self.set_registry_key(path, "Desktop", "WineDesktop")
            default_resolution = "x".join(DISPLAY_MANAGER.get_current_resolution())
            logger.debug(
                "Enabling wine virtual desktop with default resolution of %s",
                default_resolution,
            )
            self.set_registry_key(
                self.hkcu_prefix + "/Software/Wine/Explorer/Desktops",
                "WineDesktop",
                default_resolution,
            )
        else:
            self.clear_registry_key(path)

    def set_desktop_size(self, desktop_size):
        """Sets the desktop size if one is given but do not reset the key if
        one isn't.
        """
        path = self.hkcu_prefix + "/Software/Wine/Explorer/Desktops"
        if desktop_size:
            self.set_registry_key(path, "WineDesktop", desktop_size)

    def set_dpi(self, dpi):
        """Sets the DPI for WINE to use. 96 DPI is effectively unscaled."""
        self.set_registry_key(self.hkcu_prefix + "/Software/Wine/Fonts", "LogPixels", dpi)
        self.set_registry_key(self.hkcu_prefix + "/Control Panel/Desktop", "LogPixels", dpi)

    def configure_joypads(self):
        """Disables some joypad devices"""
        key = self.hkcu_prefix + "/Software/Wine/DirectInput/Joysticks"
        self.clear_registry_key(key)
        for _device, joypad_name in joypad.get_joypads():
            # Attempt at disabling mice that register as joysticks.
            # Although, those devices aren't returned by `get_joypads`
            # A better way would be to read /dev/input files directly.
            if "HARPOON RGB" in joypad_name:
                self.set_registry_key(key, "{} (js)".format(joypad_name), "disabled")
                self.set_registry_key(key, "{} (event)".format(joypad_name), "disabled")

        # This part of the code below avoids having 2 joystick interfaces
        # showing up simulatenously. It is not sure if it's still needed
        # so it is disabled for now. Street Fighter IV now runs in Proton
        # without this sort of hack.
        #
        # for device, joypad_name in joypads:
        #     if "event" in device:
        #         disabled_joypad = "{} (js)".format(joypad_name)
        #     else:
        #         disabled_joypad = "{} (event)".format(joypad_name)
        #     self.set_registry_key(key, disabled_joypad, "disabled")
appdata_dir property readonly

Returns the app-data directory for the user; this depends on a registry key.

hkcu_prefix
hklm_prefix
user_dir property readonly

Returns the directory that contains the current user's profile in the WINE prefix.

__init__(self, path) special
Source code in lutris/util/wine/prefix.py
def __init__(self, path):
    if not path:
        logger.warning("No path specified for Wine prefix")
    self.path = path
clear_registry_key(self, key)
Source code in lutris/util/wine/prefix.py
def clear_registry_key(self, key):
    registry = WineRegistry(self.get_registry_path(key))
    registry.clear_key(self.get_key_path(key))
    registry.save()
clear_registry_subkeys(self, key, subkeys)
Source code in lutris/util/wine/prefix.py
def clear_registry_subkeys(self, key, subkeys):
    registry = WineRegistry(self.get_registry_path(key))
    registry.clear_subkeys(self.get_key_path(key), subkeys)
    registry.save()
configure_joypads(self)

Disables some joypad devices

Source code in lutris/util/wine/prefix.py
def configure_joypads(self):
    """Disables some joypad devices"""
    key = self.hkcu_prefix + "/Software/Wine/DirectInput/Joysticks"
    self.clear_registry_key(key)
    for _device, joypad_name in joypad.get_joypads():
        # Attempt at disabling mice that register as joysticks.
        # Although, those devices aren't returned by `get_joypads`
        # A better way would be to read /dev/input files directly.
        if "HARPOON RGB" in joypad_name:
            self.set_registry_key(key, "{} (js)".format(joypad_name), "disabled")
            self.set_registry_key(key, "{} (event)".format(joypad_name), "disabled")

    # This part of the code below avoids having 2 joystick interfaces
    # showing up simulatenously. It is not sure if it's still needed
    # so it is disabled for now. Street Fighter IV now runs in Proton
    # without this sort of hack.
    #
    # for device, joypad_name in joypads:
    #     if "event" in device:
    #         disabled_joypad = "{} (js)".format(joypad_name)
    #     else:
    #         disabled_joypad = "{} (event)".format(joypad_name)
    #     self.set_registry_key(key, disabled_joypad, "disabled")
desktop_integration(self, desktop_dir=None, restore=False)

Overwrite desktop integration

Source code in lutris/util/wine/prefix.py
def desktop_integration(self, desktop_dir=None, restore=False):  # noqa: C901
    """Overwrite desktop integration"""
    # pylint: disable=too-many-branches
    # TODO: reduce complexity (18)
    user_dir = self.user_dir
    desktop_folders = self.get_desktop_folders()
    desktop_dir = os.path.expanduser(desktop_dir) if desktop_dir else user_dir

    if system.path_exists(user_dir):
        # Replace or restore desktop integration symlinks
        for i, item in enumerate(desktop_folders):
            path = os.path.join(user_dir, item)
            old_path = path + ".winecfg"

            if os.path.islink(path):
                if not restore:
                    os.unlink(path)
            elif os.path.isdir(path):
                try:
                    os.rmdir(path)
                # We can't delete nonempty dir, so we rename as wine do.
                except OSError:
                    os.rename(path, old_path)

            # if we want to create a symlink and one is already there, just
            # skip to the next item.  this also makes sure the elif doesn't
            # find a dir (isdir only looks at the target of the symlink).
            if restore and os.path.islink(path):
                continue
            if restore and not os.path.isdir(path):
                src_path = get_xdg_entry(DESKTOP_XDG[i])
                if not src_path:
                    logger.error("No XDG entry found for %s, launcher not created", DESKTOP_XDG[i])
                else:
                    os.symlink(src_path, path)
                # We don't need all the others process of the loop
                continue

            if desktop_dir != user_dir:
                try:
                    src_path = os.path.join(desktop_dir, item)
                except TypeError as ex:
                    # There is supposedly a None value in there
                    # The current code shouldn't allow that
                    # Just raise a exception with the values
                    raise RuntimeError("Missing value desktop_dir=%s or item=%s" % (desktop_dir, item)) from ex

                os.makedirs(src_path, exist_ok=True)
                os.symlink(src_path, path)
            else:
                # We use first the renamed dir, otherwise we make it.
                if os.path.isdir(old_path):
                    os.rename(old_path, path)
                else:
                    os.makedirs(path, exist_ok=True)
get_desktop_folders(self)

Return the list of desktop folder names loaded from the Windows registry

Source code in lutris/util/wine/prefix.py
def get_desktop_folders(self):
    """Return the list of desktop folder names loaded from the Windows registry"""
    desktop_folders = []
    for key in DESKTOP_KEYS:
        folder = self.get_registry_key(
            self.hkcu_prefix + "/Software/Microsoft/Windows/CurrentVersion/Explorer/Shell Folders",
            key,
        )
        if not folder:
            logger.warning("Couldn't load shell folder name for %s", key)
            continue
        desktop_folders.append(folder[folder.rfind("\\") + 1:])
    return desktop_folders or DEFAULT_DESKTOP_FOLDERS
get_key_path(self, key)
Source code in lutris/util/wine/prefix.py
def get_key_path(self, key):
    for prefix in (self.hkcu_prefix, self.hklm_prefix):
        if key.startswith(prefix):
            return key[len(prefix) + 1:]
    raise ValueError("The key {} is currently not supported by WinePrefixManager".format(key))
get_registry_key(self, key, subkey)
Source code in lutris/util/wine/prefix.py
def get_registry_key(self, key, subkey):
    registry = WineRegistry(self.get_registry_path(key))
    return registry.query(self.get_key_path(key), subkey)
get_registry_path(self, key)

Matches registry keys to a registry file

Currently, only HKEY_CURRENT_USER keys are supported.

Source code in lutris/util/wine/prefix.py
def get_registry_path(self, key):
    """Matches registry keys to a registry file

    Currently, only HKEY_CURRENT_USER keys are supported.
    """
    if key.startswith(self.hkcu_prefix):
        return os.path.join(self.path, "user.reg")
    if key.startswith(self.hklm_prefix):
        return os.path.join(self.path, "system.reg")
    raise ValueError("Unsupported key '{}'".format(key))
override_dll(self, dll, mode)
Source code in lutris/util/wine/prefix.py
def override_dll(self, dll, mode):
    key = self.hkcu_prefix + "/Software/Wine/DllOverrides"
    if mode.startswith("dis"):
        mode = ""
    if mode not in ("builtin", "native", "builtin,native", "native,builtin", ""):
        logger.error("DLL override '%s' mode is not valid", mode)
        return
    self.set_registry_key(key, dll, mode)
set_crash_dialogs(self, enabled)

Enable or diable Wine crash dialogs

Source code in lutris/util/wine/prefix.py
def set_crash_dialogs(self, enabled):
    """Enable or diable Wine crash dialogs"""
    self.set_registry_key(
        self.hkcu_prefix + "/Software/Wine/WineDbg",
        "ShowCrashDialog",
        1 if enabled else 0,
    )
set_desktop_size(self, desktop_size)

Sets the desktop size if one is given but do not reset the key if one isn't.

Source code in lutris/util/wine/prefix.py
def set_desktop_size(self, desktop_size):
    """Sets the desktop size if one is given but do not reset the key if
    one isn't.
    """
    path = self.hkcu_prefix + "/Software/Wine/Explorer/Desktops"
    if desktop_size:
        self.set_registry_key(path, "WineDesktop", desktop_size)
set_dpi(self, dpi)

Sets the DPI for WINE to use. 96 DPI is effectively unscaled.

Source code in lutris/util/wine/prefix.py
def set_dpi(self, dpi):
    """Sets the DPI for WINE to use. 96 DPI is effectively unscaled."""
    self.set_registry_key(self.hkcu_prefix + "/Software/Wine/Fonts", "LogPixels", dpi)
    self.set_registry_key(self.hkcu_prefix + "/Control Panel/Desktop", "LogPixels", dpi)
set_registry_key(self, key, subkey, value)
Source code in lutris/util/wine/prefix.py
def set_registry_key(self, key, subkey, value):
    registry = WineRegistry(self.get_registry_path(key))
    registry.set_value(self.get_key_path(key), subkey, value)
    registry.save()
set_virtual_desktop(self, enabled)

Enable or disable wine virtual desktop. The Lutris virtual desktop is refered to as 'WineDesktop', in Wine the virtual desktop name is 'default'.

Source code in lutris/util/wine/prefix.py
def set_virtual_desktop(self, enabled):
    """Enable or disable wine virtual desktop.
    The Lutris virtual desktop is refered to as 'WineDesktop', in Wine the
    virtual desktop name is 'default'.
    """
    path = self.hkcu_prefix + "/Software/Wine/Explorer"
    if enabled:
        self.set_registry_key(path, "Desktop", "WineDesktop")
        default_resolution = "x".join(DISPLAY_MANAGER.get_current_resolution())
        logger.debug(
            "Enabling wine virtual desktop with default resolution of %s",
            default_resolution,
        )
        self.set_registry_key(
            self.hkcu_prefix + "/Software/Wine/Explorer/Desktops",
            "WineDesktop",
            default_resolution,
        )
    else:
        self.clear_registry_key(path)
setup_defaults(self)

Sets the defaults for newly created prefixes

Source code in lutris/util/wine/prefix.py
def setup_defaults(self):
    """Sets the defaults for newly created prefixes"""
    for dll, value in DEFAULT_DLL_OVERRIDES.items():
        self.override_dll(dll, value)
    try:
        self.desktop_integration()
    except OSError as ex:
        logger.error("Failed to setup desktop integration, the prefix may not be valid.")
        logger.exception(ex)
find_prefix(path)

Given an executable path, try to find a Wine prefix associated with it.

Source code in lutris/util/wine/prefix.py
def find_prefix(path):
    """Given an executable path, try to find a Wine prefix associated with it."""
    dir_path = path
    if not dir_path:
        logger.info("No path given, unable to guess prefix location")
        return
    while dir_path != "/" and dir_path:
        dir_path = os.path.dirname(dir_path)
        if is_prefix(dir_path):
            return dir_path
        for prefix_dir in ("prefix", "pfx"):
            prefix_path = os.path.join(dir_path, prefix_dir)
            if is_prefix(prefix_path):
                return prefix_path
is_prefix(path)

Return True if the path is prefix

Source code in lutris/util/wine/prefix.py
def is_prefix(path):
    """Return True if the path is prefix"""
    return os.path.isdir(os.path.join(path, "drive_c")) \
        and os.path.exists(os.path.join(path, "user.reg"))

registry

Manipulate Wine registry files

DATA_TYPES
REG_BINARY
REG_DWORD
REG_DWORD_BIG_ENDIAN
REG_EXPAND_SZ
REG_MULTI_SZ
REG_NONE
REG_SZ
WindowsFileTime

Utility class to deal with Windows FILETIME structures.

See: https://msdn.microsoft.com/en-us/library/ms724284(v=vs.85).aspx

Source code in lutris/util/wine/registry.py
class WindowsFileTime:

    """Utility class to deal with Windows FILETIME structures.

    See: https://msdn.microsoft.com/en-us/library/ms724284(v=vs.85).aspx
    """

    ticks_per_seconds = 10000000  # 1 tick every 100 nanoseconds
    epoch_delta = 11644473600  # 3600 * 24 * ((1970 - 1601) * 365 + 89)

    def __init__(self, timestamp=None):
        self.timestamp = timestamp

    def __repr__(self):
        return "<{}>: {}".format(self.__class__.__name__, self.timestamp)

    @classmethod
    def from_hex(cls, hexvalue):
        timestamp = int(hexvalue, 16)
        return WindowsFileTime(timestamp)

    def to_hex(self):
        return "{:x}".format(self.timestamp)

    @classmethod
    def from_unix_timestamp(cls, timestamp):
        timestamp = timestamp + cls.epoch_delta
        timestamp = int(timestamp * cls.ticks_per_seconds)
        return WindowsFileTime(timestamp)

    def to_unix_timestamp(self):
        if not self.timestamp:
            raise ValueError("No timestamp set")
        unix_ts = self.timestamp / self.ticks_per_seconds
        unix_ts = unix_ts - self.epoch_delta
        return unix_ts

    def to_date_time(self):
        return datetime.fromtimestamp(self.to_unix_timestamp())
epoch_delta
ticks_per_seconds
__init__(self, timestamp=None) special
Source code in lutris/util/wine/registry.py
def __init__(self, timestamp=None):
    self.timestamp = timestamp
__repr__(self) special
Source code in lutris/util/wine/registry.py
def __repr__(self):
    return "<{}>: {}".format(self.__class__.__name__, self.timestamp)
from_hex(hexvalue) classmethod
Source code in lutris/util/wine/registry.py
@classmethod
def from_hex(cls, hexvalue):
    timestamp = int(hexvalue, 16)
    return WindowsFileTime(timestamp)
from_unix_timestamp(timestamp) classmethod
Source code in lutris/util/wine/registry.py
@classmethod
def from_unix_timestamp(cls, timestamp):
    timestamp = timestamp + cls.epoch_delta
    timestamp = int(timestamp * cls.ticks_per_seconds)
    return WindowsFileTime(timestamp)
to_date_time(self)
Source code in lutris/util/wine/registry.py
def to_date_time(self):
    return datetime.fromtimestamp(self.to_unix_timestamp())
to_hex(self)
Source code in lutris/util/wine/registry.py
def to_hex(self):
    return "{:x}".format(self.timestamp)
to_unix_timestamp(self)
Source code in lutris/util/wine/registry.py
def to_unix_timestamp(self):
    if not self.timestamp:
        raise ValueError("No timestamp set")
    unix_ts = self.timestamp / self.ticks_per_seconds
    unix_ts = unix_ts - self.epoch_delta
    return unix_ts
WineRegistry
Source code in lutris/util/wine/registry.py
class WineRegistry:
    version_header = "WINE REGISTRY Version "
    relative_to_header = ";; All keys relative to "

    def __init__(self, reg_filename=None):
        self.arch = WINE_DEFAULT_ARCH
        self.version = 2
        self.relative_to = "\\\\User\\\\S-1-5-21-0-0-0-1000"
        self.keys = OrderedDict()
        self.reg_filename = reg_filename
        if reg_filename:
            if not system.path_exists(reg_filename):
                logger.error("No registry file at %s", reg_filename)
            self.parse_reg_file(reg_filename)

    def __str__(self):
        return "Windows Registry @ %s" % self.reg_filename

    @property
    def prefix_path(self):
        """Return the Wine prefix path (where the .reg files are located)"""
        if self.reg_filename:
            return os.path.dirname(self.reg_filename)
        return None

    @staticmethod
    def get_raw_registry(reg_filename):
        """Return an array of the unprocessed contents of a registry file"""
        if not system.path_exists(reg_filename):
            return []
        with open(reg_filename, "r", encoding='utf-8') as reg_file:

            try:
                registry_content = reg_file.readlines()
            except Exception:  # pylint: disable=broad-except
                logger.exception("Failed to registry read %s", reg_filename)
                registry_content = []
        return registry_content

    def parse_reg_file(self, reg_filename):
        registry_lines = self.get_raw_registry(reg_filename)
        current_key = None
        add_next_to_value = False
        additional_values = []
        for line in registry_lines:
            line = line.rstrip("\n")

            if line.startswith("["):
                current_key = WineRegistryKey(key_def=line)
                self.keys[current_key.name] = current_key
            elif current_key:
                if add_next_to_value:
                    additional_values.append(line)
                elif not add_next_to_value:
                    if additional_values:
                        additional_values = "\n".join(additional_values)
                        current_key.add_to_last(additional_values)
                        additional_values = []
                    current_key.parse(line)
                add_next_to_value = line.endswith("\\")
            elif line.startswith(self.version_header):
                self.version = int(line[len(self.version_header):])
            elif line.startswith(self.relative_to_header):
                self.relative_to = line[len(self.relative_to_header):]
            elif line.startswith("#arch"):
                self.arch = line.split("=")[1]

    def render(self):
        content = "{}{}\n".format(self.version_header, self.version)
        content += "{}{}\n\n".format(self.relative_to_header, self.relative_to)
        content += "#arch={}\n".format(self.arch)
        for key in self.keys:
            content += "\n"
            content += self.keys[key].render()
        return content

    def save(self, path=None):
        """Write the registry to a file"""
        if not path:
            path = self.reg_filename
        if not path:
            raise OSError("No filename provided")
        prefix_path = os.path.dirname(path)
        if not os.path.isdir(prefix_path):
            raise OSError(
                "Invalid Wine prefix path %s, make sure to "
                "create the prefix before saving to a registry" % prefix_path
            )
        with open(path, "w", encoding='utf-8') as registry_file:
            registry_file.write(self.render())

    def query(self, path, subkey):
        key = self.keys.get(path)
        if key:
            return key.get_subkey(subkey)
        return

    def set_value(self, path, subkey, value):
        key = self.keys.get(path)
        if not key:
            key = WineRegistryKey(path=path)
            self.keys[key.name] = key
        key.set_subkey(subkey, value)

    def clear_key(self, path):
        """Removes all subkeys from a key"""
        key = self.keys.get(path)
        if not key:
            return
        key.subkeys.clear()

    def clear_subkeys(self, path, keys):
        """Remove some subkeys from a key"""
        key = self.keys.get(path)
        if not key:
            return
        for subkey in list(key.subkeys.keys()):
            if subkey not in keys:
                continue
            key.subkeys.pop(subkey)

    def get_unix_path(self, windows_path):
        windows_path = windows_path.replace("\\", "/")
        if not self.prefix_path:
            return
        drives_path = os.path.join(self.prefix_path, "dosdevices")
        if not system.path_exists(drives_path):
            return
        letter, relpath = windows_path.split(":", 1)
        relpath = relpath.strip("/")
        drive_link = os.path.join(drives_path, letter.lower() + ":")
        try:
            drive_path = os.readlink(drive_link)
        except FileNotFoundError:
            logger.error("Unable to read link for %s", drive_link)
            return

        if not os.path.isabs(drive_path):
            drive_path = os.path.join(drives_path, drive_path)
        return os.path.join(drive_path, relpath)
prefix_path property readonly

Return the Wine prefix path (where the .reg files are located)

relative_to_header
version_header
__init__(self, reg_filename=None) special
Source code in lutris/util/wine/registry.py
def __init__(self, reg_filename=None):
    self.arch = WINE_DEFAULT_ARCH
    self.version = 2
    self.relative_to = "\\\\User\\\\S-1-5-21-0-0-0-1000"
    self.keys = OrderedDict()
    self.reg_filename = reg_filename
    if reg_filename:
        if not system.path_exists(reg_filename):
            logger.error("No registry file at %s", reg_filename)
        self.parse_reg_file(reg_filename)
__str__(self) special
Source code in lutris/util/wine/registry.py
def __str__(self):
    return "Windows Registry @ %s" % self.reg_filename
clear_key(self, path)

Removes all subkeys from a key

Source code in lutris/util/wine/registry.py
def clear_key(self, path):
    """Removes all subkeys from a key"""
    key = self.keys.get(path)
    if not key:
        return
    key.subkeys.clear()
clear_subkeys(self, path, keys)

Remove some subkeys from a key

Source code in lutris/util/wine/registry.py
def clear_subkeys(self, path, keys):
    """Remove some subkeys from a key"""
    key = self.keys.get(path)
    if not key:
        return
    for subkey in list(key.subkeys.keys()):
        if subkey not in keys:
            continue
        key.subkeys.pop(subkey)
get_raw_registry(reg_filename) staticmethod

Return an array of the unprocessed contents of a registry file

Source code in lutris/util/wine/registry.py
@staticmethod
def get_raw_registry(reg_filename):
    """Return an array of the unprocessed contents of a registry file"""
    if not system.path_exists(reg_filename):
        return []
    with open(reg_filename, "r", encoding='utf-8') as reg_file:

        try:
            registry_content = reg_file.readlines()
        except Exception:  # pylint: disable=broad-except
            logger.exception("Failed to registry read %s", reg_filename)
            registry_content = []
    return registry_content
get_unix_path(self, windows_path)
Source code in lutris/util/wine/registry.py
def get_unix_path(self, windows_path):
    windows_path = windows_path.replace("\\", "/")
    if not self.prefix_path:
        return
    drives_path = os.path.join(self.prefix_path, "dosdevices")
    if not system.path_exists(drives_path):
        return
    letter, relpath = windows_path.split(":", 1)
    relpath = relpath.strip("/")
    drive_link = os.path.join(drives_path, letter.lower() + ":")
    try:
        drive_path = os.readlink(drive_link)
    except FileNotFoundError:
        logger.error("Unable to read link for %s", drive_link)
        return

    if not os.path.isabs(drive_path):
        drive_path = os.path.join(drives_path, drive_path)
    return os.path.join(drive_path, relpath)
parse_reg_file(self, reg_filename)
Source code in lutris/util/wine/registry.py
def parse_reg_file(self, reg_filename):
    registry_lines = self.get_raw_registry(reg_filename)
    current_key = None
    add_next_to_value = False
    additional_values = []
    for line in registry_lines:
        line = line.rstrip("\n")

        if line.startswith("["):
            current_key = WineRegistryKey(key_def=line)
            self.keys[current_key.name] = current_key
        elif current_key:
            if add_next_to_value:
                additional_values.append(line)
            elif not add_next_to_value:
                if additional_values:
                    additional_values = "\n".join(additional_values)
                    current_key.add_to_last(additional_values)
                    additional_values = []
                current_key.parse(line)
            add_next_to_value = line.endswith("\\")
        elif line.startswith(self.version_header):
            self.version = int(line[len(self.version_header):])
        elif line.startswith(self.relative_to_header):
            self.relative_to = line[len(self.relative_to_header):]
        elif line.startswith("#arch"):
            self.arch = line.split("=")[1]
query(self, path, subkey)
Source code in lutris/util/wine/registry.py
def query(self, path, subkey):
    key = self.keys.get(path)
    if key:
        return key.get_subkey(subkey)
    return
render(self)
Source code in lutris/util/wine/registry.py
def render(self):
    content = "{}{}\n".format(self.version_header, self.version)
    content += "{}{}\n\n".format(self.relative_to_header, self.relative_to)
    content += "#arch={}\n".format(self.arch)
    for key in self.keys:
        content += "\n"
        content += self.keys[key].render()
    return content
save(self, path=None)

Write the registry to a file

Source code in lutris/util/wine/registry.py
def save(self, path=None):
    """Write the registry to a file"""
    if not path:
        path = self.reg_filename
    if not path:
        raise OSError("No filename provided")
    prefix_path = os.path.dirname(path)
    if not os.path.isdir(prefix_path):
        raise OSError(
            "Invalid Wine prefix path %s, make sure to "
            "create the prefix before saving to a registry" % prefix_path
        )
    with open(path, "w", encoding='utf-8') as registry_file:
        registry_file.write(self.render())
set_value(self, path, subkey, value)
Source code in lutris/util/wine/registry.py
def set_value(self, path, subkey, value):
    key = self.keys.get(path)
    if not key:
        key = WineRegistryKey(path=path)
        self.keys[key.name] = key
    key.set_subkey(subkey, value)
WineRegistryKey
Source code in lutris/util/wine/registry.py
class WineRegistryKey:

    def __init__(self, key_def=None, path=None):

        self.subkeys = OrderedDict()
        self.metas = OrderedDict()

        if path:
            # Key is created by path, it's a new key
            timestamp = datetime.now().timestamp()
            self.name = path
            self.raw_name = "[{}]".format(path.replace("/", "\\\\"))
            self.raw_timestamp = " ".join(str(timestamp).split("."))

            windows_timestamp = WindowsFileTime.from_unix_timestamp(timestamp)
            self.metas["time"] = windows_timestamp.to_hex()
        else:
            # Existing key loaded from file
            self.raw_name, self.raw_timestamp = re.split(re.compile(r"(?<=[^\\]\]) "), key_def, maxsplit=1)
            self.name = self.raw_name.replace("\\\\", "/").strip("[]")

        # Parse timestamp either as int or float
        ts_parts = self.raw_timestamp.strip().split()
        if len(ts_parts) == 1:
            self.timestamp = int(ts_parts[0])
        else:
            self.timestamp = float("{}.{}".format(ts_parts[0], ts_parts[1]))

    def __str__(self):
        return "{0} {1}".format(self.raw_name, self.raw_timestamp)

    def parse(self, line):
        """Parse a registry line, populating meta and subkeys"""
        if len(line) < 4:
            # Line is too short, nothing to parse
            return

        if line.startswith("#"):
            self.add_meta(line)
        elif line.startswith('"'):
            try:
                key, value = re.split(re.compile(r"(?<![^\\]\\\")="), line, maxsplit=1)
            except ValueError as ex:
                logger.error("Unable to parse line %s", line)
                logger.exception(ex)
                return
            key = key[1:-1]
            self.subkeys[key] = value
        elif line.startswith("@"):
            key, value = line.split("=", 1)
            self.subkeys["default"] = value

    def add_to_last(self, line):
        try:
            last_subkey = next(reversed(self.subkeys))
        except StopIteration:
            logger.warning("Should this be happening?")
            return
        self.subkeys[last_subkey] += "\n{}".format(line)

    def render(self):
        """Return the content of the key in the wine .reg format"""
        content = self.raw_name + " " + self.raw_timestamp + "\n"
        for key, value in self.metas.items():
            if value is None:
                content += "#{}\n".format(key)
            else:
                content += "#{}={}\n".format(key, value)
        for key, value in self.subkeys.items():
            if key == "default":
                key = "@"
            else:
                key = '"{}"'.format(key)
            content += "{}={}\n".format(key, value)
        return content

    def render_value(self, value):
        if isinstance(value, int):
            return "dword:{:08x}".format(value)
        if isinstance(value, str):
            return '"{}"'.format(value)
        raise NotImplementedError("TODO")

    @staticmethod
    def decode_unicode(string):
        # There may be a r"\\" in front of r"\x", so replace the r"\\" to r"\x005c"
        # to avoid missing matches. Example: r"C:\\users\\x1234\\\x0041\x0042CD".
        # Note the difference between r"\\x1234", r"\\\x0041" and r"\x0042".
        # It should be r"C:\users\x1234\ABCD" after decoding.
        chunks = re.split(r"\\x", string.replace(r"\\", r"\x005c"))
        out = chunks.pop(0).encode().decode("unicode_escape")
        for chunk in chunks:
            # We have seen file with unicode characters escaped on 1 byte (\xfa),
            # 1.5 bytes (\x444) and 2 bytes (\x00ed). So we try 0 padding, 1 and 2
            # (python wants its escaped sequence to be exactly on 4 characters).
            # The exception let us know if it worked or not
            for i in [0, 1, 2]:
                try:
                    out += ("\\u{}{}".format("0" * i, chunk).encode().decode("unicode_escape"))
                    break
                except UnicodeDecodeError:
                    pass
        return out

    def add_meta(self, meta_line):
        if not meta_line.startswith("#"):
            raise ValueError("Key metas should start with '#'")
        meta_line = meta_line[1:]
        parts = meta_line.split("=")
        if len(parts) == 2:
            key = parts[0]
            value = parts[1]
        elif len(parts) == 1:
            key = parts[0]
            value = None
        else:
            raise ValueError("Invalid meta line '{}'".format(meta_line))
        self.metas[key] = value

    def get_meta(self, name):
        return self.metas.get(name)

    def set_subkey(self, name, value):
        self.subkeys[name] = self.render_value(value)

    def get_subkey(self, name):
        if name not in self.subkeys:
            return None
        value = self.subkeys[name]
        if value.startswith('"') and value.endswith('"'):
            return self.decode_unicode(value[1:-1])
        if value.startswith("dword:"):
            return int(value[6:], 16)
        raise ValueError("Handle %s" % value)
__init__(self, key_def=None, path=None) special
Source code in lutris/util/wine/registry.py
def __init__(self, key_def=None, path=None):

    self.subkeys = OrderedDict()
    self.metas = OrderedDict()

    if path:
        # Key is created by path, it's a new key
        timestamp = datetime.now().timestamp()
        self.name = path
        self.raw_name = "[{}]".format(path.replace("/", "\\\\"))
        self.raw_timestamp = " ".join(str(timestamp).split("."))

        windows_timestamp = WindowsFileTime.from_unix_timestamp(timestamp)
        self.metas["time"] = windows_timestamp.to_hex()
    else:
        # Existing key loaded from file
        self.raw_name, self.raw_timestamp = re.split(re.compile(r"(?<=[^\\]\]) "), key_def, maxsplit=1)
        self.name = self.raw_name.replace("\\\\", "/").strip("[]")

    # Parse timestamp either as int or float
    ts_parts = self.raw_timestamp.strip().split()
    if len(ts_parts) == 1:
        self.timestamp = int(ts_parts[0])
    else:
        self.timestamp = float("{}.{}".format(ts_parts[0], ts_parts[1]))
__str__(self) special
Source code in lutris/util/wine/registry.py
def __str__(self):
    return "{0} {1}".format(self.raw_name, self.raw_timestamp)
add_meta(self, meta_line)
Source code in lutris/util/wine/registry.py
def add_meta(self, meta_line):
    if not meta_line.startswith("#"):
        raise ValueError("Key metas should start with '#'")
    meta_line = meta_line[1:]
    parts = meta_line.split("=")
    if len(parts) == 2:
        key = parts[0]
        value = parts[1]
    elif len(parts) == 1:
        key = parts[0]
        value = None
    else:
        raise ValueError("Invalid meta line '{}'".format(meta_line))
    self.metas[key] = value
add_to_last(self, line)
Source code in lutris/util/wine/registry.py
def add_to_last(self, line):
    try:
        last_subkey = next(reversed(self.subkeys))
    except StopIteration:
        logger.warning("Should this be happening?")
        return
    self.subkeys[last_subkey] += "\n{}".format(line)
decode_unicode(string) staticmethod
Source code in lutris/util/wine/registry.py
@staticmethod
def decode_unicode(string):
    # There may be a r"\\" in front of r"\x", so replace the r"\\" to r"\x005c"
    # to avoid missing matches. Example: r"C:\\users\\x1234\\\x0041\x0042CD".
    # Note the difference between r"\\x1234", r"\\\x0041" and r"\x0042".
    # It should be r"C:\users\x1234\ABCD" after decoding.
    chunks = re.split(r"\\x", string.replace(r"\\", r"\x005c"))
    out = chunks.pop(0).encode().decode("unicode_escape")
    for chunk in chunks:
        # We have seen file with unicode characters escaped on 1 byte (\xfa),
        # 1.5 bytes (\x444) and 2 bytes (\x00ed). So we try 0 padding, 1 and 2
        # (python wants its escaped sequence to be exactly on 4 characters).
        # The exception let us know if it worked or not
        for i in [0, 1, 2]:
            try:
                out += ("\\u{}{}".format("0" * i, chunk).encode().decode("unicode_escape"))
                break
            except UnicodeDecodeError:
                pass
    return out
get_meta(self, name)
Source code in lutris/util/wine/registry.py
def get_meta(self, name):
    return self.metas.get(name)
get_subkey(self, name)
Source code in lutris/util/wine/registry.py
def get_subkey(self, name):
    if name not in self.subkeys:
        return None
    value = self.subkeys[name]
    if value.startswith('"') and value.endswith('"'):
        return self.decode_unicode(value[1:-1])
    if value.startswith("dword:"):
        return int(value[6:], 16)
    raise ValueError("Handle %s" % value)
parse(self, line)

Parse a registry line, populating meta and subkeys

Source code in lutris/util/wine/registry.py
def parse(self, line):
    """Parse a registry line, populating meta and subkeys"""
    if len(line) < 4:
        # Line is too short, nothing to parse
        return

    if line.startswith("#"):
        self.add_meta(line)
    elif line.startswith('"'):
        try:
            key, value = re.split(re.compile(r"(?<![^\\]\\\")="), line, maxsplit=1)
        except ValueError as ex:
            logger.error("Unable to parse line %s", line)
            logger.exception(ex)
            return
        key = key[1:-1]
        self.subkeys[key] = value
    elif line.startswith("@"):
        key, value = line.split("=", 1)
        self.subkeys["default"] = value
render(self)

Return the content of the key in the wine .reg format

Source code in lutris/util/wine/registry.py
def render(self):
    """Return the content of the key in the wine .reg format"""
    content = self.raw_name + " " + self.raw_timestamp + "\n"
    for key, value in self.metas.items():
        if value is None:
            content += "#{}\n".format(key)
        else:
            content += "#{}={}\n".format(key, value)
    for key, value in self.subkeys.items():
        if key == "default":
            key = "@"
        else:
            key = '"{}"'.format(key)
        content += "{}={}\n".format(key, value)
    return content
render_value(self, value)
Source code in lutris/util/wine/registry.py
def render_value(self, value):
    if isinstance(value, int):
        return "dword:{:08x}".format(value)
    if isinstance(value, str):
        return '"{}"'.format(value)
    raise NotImplementedError("TODO")
set_subkey(self, name, value)
Source code in lutris/util/wine/registry.py
def set_subkey(self, name, value):
    self.subkeys[name] = self.render_value(value)

vkd3d

VKD3DManager (DLLManager)
Source code in lutris/util/wine/vkd3d.py
class VKD3DManager(DLLManager):
    component = "VKD3D"
    base_dir = os.path.join(RUNTIME_DIR, "vkd3d")
    versions_path = os.path.join(base_dir, "vkd3d_versions.json")
    managed_dlls = ("d3d12", )
    releases_url = "https://api.github.com/repos/lutris/vkd3d/releases"
base_dir
component
managed_dlls
releases_url
versions_path

wine

Utilities for manipulating Wine

ESYNC_LIMIT_CHECK
FSYNC_SUPPORT_CHECK
POL_PATH
WINE_DEFAULT_ARCH
WINE_DIR
WINE_PATHS
detect_arch(prefix_path=None, wine_path=None)

Given a Wine prefix path, return its architecture

Source code in lutris/util/wine/wine.py
def detect_arch(prefix_path=None, wine_path=None):
    """Given a Wine prefix path, return its architecture"""
    arch = detect_prefix_arch(prefix_path)
    if arch:
        return arch
    if wine_path and system.path_exists(wine_path + "64"):
        return "win64"
    return "win32"
detect_prefix_arch(prefix_path=None)

Return the architecture of the prefix found in prefix_path.

If no prefix_path given, return the arch of the system's default prefix. If no prefix found, return None.

Source code in lutris/util/wine/wine.py
def detect_prefix_arch(prefix_path=None):
    """Return the architecture of the prefix found in `prefix_path`.

    If no `prefix_path` given, return the arch of the system's default prefix.
    If no prefix found, return None."""
    if not prefix_path:
        prefix_path = "~/.wine"
    prefix_path = os.path.expanduser(prefix_path)
    registry_path = os.path.join(prefix_path, "system.reg")
    if not os.path.isdir(prefix_path) or not os.path.isfile(registry_path):
        # No prefix_path exists or invalid prefix
        logger.debug("Prefix not found: %s", prefix_path)
        return None
    with open(registry_path, "r", encoding='utf-8') as registry:
        for _line_no in range(5):
            line = registry.readline()
            if "win64" in line:
                return "win64"
            if "win32" in line:
                return "win32"
    logger.debug("Failed to detect Wine prefix architecture in %s", prefix_path)
    return None
display_vulkan_error(on_launch)
Source code in lutris/util/wine/wine.py
def display_vulkan_error(on_launch):
    if on_launch:
        checkbox_message = _("Launch anyway and do not show this message again.")
    else:
        checkbox_message = _("Enable anyway and do not show this message again.")

    setting = "hide-no-vulkan-warning"
    DontShowAgainDialog(
        setting,
        _("Vulkan is not installed or is not supported by your system"),
        secondary_message=_(
            "If you have compatible hardware, please follow "
            "the installation procedures as described in\n"
            "<a href='https://github.com/lutris/lutris/wiki/How-to:-DXVK'>"
            "How-to:-DXVK (https://github.com/lutris/lutris/wiki/How-to:-DXVK)</a>"
        ),
        checkbox_message=checkbox_message,
    )
    return settings.read_setting(setting) == "True"
esync_display_limit_warning()
Source code in lutris/util/wine/wine.py
def esync_display_limit_warning():
    ErrorDialog(_(
        "Your limits are not set correctly."
        " Please increase them as described here:"
        " <a href='https://github.com/lutris/lutris/wiki/How-to:-Esync'>"
        "How-to:-Esync (https://github.com/lutris/lutris/wiki/How-to:-Esync)</a>"
    ))
esync_display_version_warning(on_launch=False)
Source code in lutris/util/wine/wine.py
def esync_display_version_warning(on_launch=False):
    setting = "hide-wine-non-esync-version-warning"
    if on_launch:
        checkbox_message = _("Launch anyway and do not show this message again.")
    else:
        checkbox_message = _("Enable anyway and do not show this message again.")

    DontShowAgainDialog(
        setting,
        _("Incompatible Wine version detected"),
        secondary_message=_(
            "The Wine build you have selected "
            "does not support Esync.\n"
            "Please switch to an Esync-capable version."
        ),
        checkbox_message=checkbox_message,
    )
    return settings.read_setting(setting) == "True"
fsync_display_support_warning()
Source code in lutris/util/wine/wine.py
def fsync_display_support_warning():
    ErrorDialog(_(
        "Your kernel is not patched for fsync."
        " Please get a patched kernel to use fsync."
    ))
fsync_display_version_warning(on_launch=False)
Source code in lutris/util/wine/wine.py
def fsync_display_version_warning(on_launch=False):
    setting = "hide-wine-non-fsync-version-warning"
    if on_launch:
        checkbox_message = _("Launch anyway and do not show this message again.")
    else:
        checkbox_message = _("Enable anyway and do not show this message again.")

    DontShowAgainDialog(
        setting,
        _("Incompatible Wine version detected"),
        secondary_message=_(
            "The Wine build you have selected "
            "does not support Fsync.\n"
            "Please switch to an Fsync-capable version."
        ),
        checkbox_message=checkbox_message,
    )
    return settings.read_setting(setting) == "True"
get_default_version()

Return the default version of wine. Prioritize 64bit builds

Source code in lutris/util/wine/wine.py
def get_default_version():
    """Return the default version of wine. Prioritize 64bit builds"""
    installed_versions = get_wine_versions()
    wine64_versions = [version for version in installed_versions if "64" in version]
    if wine64_versions:
        return wine64_versions[0]
    if installed_versions:
        return installed_versions[0]
    return
get_lutris_wine_versions()

Return the list of wine versions installed by lutris

Source code in lutris/util/wine/wine.py
def get_lutris_wine_versions():
    """Return the list of wine versions installed by lutris"""
    versions = []
    if system.path_exists(WINE_DIR):
        dirs = version_sort(os.listdir(WINE_DIR), reverse=True)
        for dirname in dirs:
            if is_version_installed(dirname):
                versions.append(dirname)
    return versions
get_overrides_env(overrides)

Output a string of dll overrides usable with WINEDLLOVERRIDES See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides

Source code in lutris/util/wine/wine.py
def get_overrides_env(overrides):
    """
    Output a string of dll overrides usable with WINEDLLOVERRIDES
    See: https://wiki.winehq.org/Wine_User%27s_Guide#WINEDLLOVERRIDES.3DDLL_Overrides
    """
    default_overrides = {
        "winemenubuilder": ""
    }
    overrides.update(default_overrides)
    override_buckets = OrderedDict([("n,b", []), ("b,n", []), ("b", []), ("n", []), ("d", []), ("", [])])
    for dll, value in overrides.items():
        if not value:
            value = ""
        value = value.replace(" ", "")
        value = value.replace("builtin", "b")
        value = value.replace("native", "n")
        value = value.replace("disabled", "")
        try:
            override_buckets[value].append(dll)
        except KeyError:
            logger.error("Invalid override value %s", value)
            continue

    override_strings = []
    for value, dlls in override_buckets.items():
        if not dlls:
            continue
        override_strings.append("{}={}".format(",".join(sorted(dlls)), value))
    return ";".join(override_strings)
get_playonlinux()

Return the folder containing PoL config files

Source code in lutris/util/wine/wine.py
def get_playonlinux():
    """Return the folder containing PoL config files"""
    pol_path = os.path.expanduser("~/.PlayOnLinux")
    if system.path_exists(os.path.join(pol_path, "wine")):
        return pol_path
    return None
get_pol_wine_versions()

Return the list of wine versions installed by Play on Linux

Source code in lutris/util/wine/wine.py
def get_pol_wine_versions():
    """Return the list of wine versions installed by Play on Linux"""
    if not POL_PATH:
        return []
    versions = []
    for arch in ['x86', 'amd64']:
        builds_path = os.path.join(POL_PATH, "wine/linux-%s" % arch)
        if not system.path_exists(builds_path):
            continue
        for version in os.listdir(builds_path):
            if system.path_exists(os.path.join(builds_path, version, "bin/wine")):
                versions.append("PlayOnLinux %s-%s" % (version, arch))
    return versions
get_proton_paths()

Get the Folder that contains all the Proton versions. Can probably be improved

Source code in lutris/util/wine/wine.py
def get_proton_paths():
    """Get the Folder that contains all the Proton versions. Can probably be improved"""
    paths = set()
    for path in _iter_proton_locations():
        proton_versions = [p for p in os.listdir(path) if "Proton" in p]
        for version in proton_versions:
            if system.path_exists(os.path.join(path, version, "dist/bin/wine")):
                paths.add(path)
    return list(paths)
get_proton_versions()

Return the list of Proton versions installed in Steam

Source code in lutris/util/wine/wine.py
def get_proton_versions():
    """Return the list of Proton versions installed in Steam"""
    versions = []
    for proton_path in get_proton_paths():
        proton_versions = [p for p in os.listdir(proton_path) if "Proton" in p]
        for version in proton_versions:
            path = os.path.join(proton_path, version, "dist/bin/wine")
            if os.path.isfile(path):
                versions.append(version)
    return versions
get_real_executable(windows_executable, working_dir=None)

Given a Windows executable, return the real program capable of launching it along with necessary arguments.

Source code in lutris/util/wine/wine.py
def get_real_executable(windows_executable, working_dir=None):
    """Given a Windows executable, return the real program
    capable of launching it along with necessary arguments."""

    exec_name = windows_executable.lower()

    if exec_name.endswith(".msi"):
        return ("msiexec", ["/i", windows_executable], working_dir)

    if exec_name.endswith(".bat"):
        if not working_dir or os.path.dirname(windows_executable) == working_dir:
            working_dir = os.path.dirname(windows_executable) or None
            windows_executable = os.path.basename(windows_executable)
        return ("cmd", ["/C", windows_executable], working_dir)

    if exec_name.endswith(".lnk"):
        return ("start", ["/unix", windows_executable], working_dir)

    return (windows_executable, [], working_dir)
get_system_wine_versions()

Return the list of wine versions installed on the system

Source code in lutris/util/wine/wine.py
def get_system_wine_versions():
    """Return the list of wine versions installed on the system"""
    versions = []
    for build in sorted(WINE_PATHS.keys()):
        version = get_wine_version(WINE_PATHS[build])
        if version:
            versions.append(build)
    return versions
get_wine_version(wine_path='wine')

Return the version of Wine installed on the system.

Source code in lutris/util/wine/wine.py
def get_wine_version(wine_path="wine"):
    """Return the version of Wine installed on the system."""
    if wine_path != "wine" and not system.path_exists(wine_path):
        return
    if wine_path == "wine" and not system.find_executable("wine"):
        return
    if os.path.isabs(wine_path):
        wine_stats = os.stat(wine_path)
        if wine_stats.st_size < 2000:
            # This version is a script, ignore it
            return
    version = system.read_process_output([wine_path, "--version"])
    if not version:
        logger.error("Error reading wine version for %s", wine_path)
        return
    if version.startswith("wine-"):
        version = version[5:]
    return version
get_wine_version_exe(version)
Source code in lutris/util/wine/wine.py
def get_wine_version_exe(version):
    if not version:
        version = get_default_version()
    if not version:
        raise RuntimeError("Wine is not installed")
    return os.path.join(WINE_DIR, "{}/bin/wine".format(version))
get_wine_versions()

Return the list of Wine versions installed

Source code in lutris/util/wine/wine.py
@lru_cache(maxsize=8)
def get_wine_versions():
    """Return the list of Wine versions installed"""
    versions = []
    versions += get_system_wine_versions()
    versions += get_lutris_wine_versions()
    if os.environ.get("LUTRIS_ENABLE_PROTON"):
        versions += get_proton_versions()
    versions += get_pol_wine_versions()
    return versions
is_esync_limit_set()

Checks if the number of files open is acceptable for esync usage.

Source code in lutris/util/wine/wine.py
def is_esync_limit_set():
    """Checks if the number of files open is acceptable for esync usage."""
    if ESYNC_LIMIT_CHECK in ("0", "off"):
        logger.info("fd limit check for esync was manually disabled")
        return True
    return linux.LINUX_SYSTEM.has_enough_file_descriptors()
is_fsync_supported()

Checks if the running kernel has Valve's futex patch applied.

Source code in lutris/util/wine/wine.py
def is_fsync_supported():
    """Checks if the running kernel has Valve's futex patch applied."""
    if FSYNC_SUPPORT_CHECK in ("0", "off"):
        logger.info("futex patch check for fsync was manually disabled")
        return True
    return fsync.is_fsync_supported()
is_gstreamer_build(wine_path)

Returns whether a wine build ships with gstreamer libraries. This allows to set GST_PLUGIN_SYSTEM_PATH_1_0 for the builds that support it.

Source code in lutris/util/wine/wine.py
def is_gstreamer_build(wine_path):
    """Returns whether a wine build ships with gstreamer libraries.
    This allows to set GST_PLUGIN_SYSTEM_PATH_1_0 for the builds that support it.
    """
    base_path = os.path.dirname(os.path.dirname(wine_path))
    return system.path_exists(os.path.join(base_path, "lib64/gstreamer-1.0"))
is_installed_systemwide()

Return whether Wine is installed outside of Lutris

Source code in lutris/util/wine/wine.py
def is_installed_systemwide():
    """Return whether Wine is installed outside of Lutris"""
    for build in WINE_PATHS.values():
        if system.find_executable(build):
            # if wine64 is installed but not wine32, don't consider it
            # a system-wide installation.
            if (
                build == "wine" and system.path_exists("/usr/lib/wine/wine64")
                and not system.path_exists("/usr/lib/wine/wine")
            ):
                logger.warning("wine32 is missing from system")
                return False
            return True
    return False
is_mingw_build(wine_path)

Returns whether a wine build is built with MingW

Source code in lutris/util/wine/wine.py
def is_mingw_build(wine_path):
    """Returns whether a wine build is built with MingW"""
    base_path = os.path.dirname(os.path.dirname(wine_path))
    # A MingW build has an .exe file while a GCC one will have a .so
    return system.path_exists(os.path.join(base_path, "lib/wine/iexplore.exe"))
is_version_esync(path)

Determines if a Wine build is Esync capable

Parameters:

Name Type Description Default
path

the path to the Wine version

required

Returns:

Type Description
bool

True is the build is Esync capable

Source code in lutris/util/wine/wine.py
def is_version_esync(path):
    """Determines if a Wine build is Esync capable

    Params:
        path: the path to the Wine version

    Returns:
        bool: True is the build is Esync capable
    """
    try:
        version = path.split("/")[-3].lower()
    except IndexError:
        logger.error("Invalid path '%s'", path)
        return False
    _version_number, version_prefix, version_suffix = parse_version(version)
    esync_compatible_versions = ["esync", "lutris", "tkg", "ge", "proton", "staging"]
    for esync_version in esync_compatible_versions:
        if esync_version in version:
            return True
    wine_version = get_wine_version(path)
    if wine_version:
        wine_version = wine_version.lower()
        return "esync" in wine_version or "staging" in wine_version
    return False
is_version_fsync(path)

Determines if a Wine build is Fsync capable

Parameters:

Name Type Description Default
path

the path to the Wine version

required

Returns:

Type Description
bool

True is the build is Fsync capable

Source code in lutris/util/wine/wine.py
def is_version_fsync(path):
    """Determines if a Wine build is Fsync capable

    Params:
        path: the path to the Wine version

    Returns:
        bool: True is the build is Fsync capable
    """
    try:
        version = path.split("/")[-3].lower()
    except IndexError:
        logger.error("Invalid path '%s'", path)
        return False
    _version_number, version_prefix, version_suffix = parse_version(version)
    fsync_compatible_versions = ["fsync", "lutris", "ge", "proton"]
    for fsync_version in fsync_compatible_versions:
        if fsync_version in version:
            return True
    wine_version = get_wine_version(path)
    if wine_version:
        return "fsync" in wine_version.lower()
    return False
is_version_installed(version)
Source code in lutris/util/wine/wine.py
def is_version_installed(version):
    return os.path.isfile(get_wine_version_exe(version))
set_drive_path(prefix, letter, path)

Changes the path to a Wine drive

Source code in lutris/util/wine/wine.py
def set_drive_path(prefix, letter, path):
    """Changes the path to a Wine drive"""
    dosdevices_path = os.path.join(prefix, "dosdevices")
    if not system.path_exists(dosdevices_path):
        raise OSError("Invalid prefix path %s" % prefix)
    drive_path = os.path.join(dosdevices_path, letter + ":")
    if system.path_exists(drive_path):
        os.remove(drive_path)
    logger.debug("Linking %s to %s", drive_path, path)
    os.symlink(path, drive_path)
use_lutris_runtime(wine_path, force_disable=False)

Returns whether to use the Lutris runtime. The runtime can be forced to be disabled, otherwise it's disabled automatically if Wine is installed system wide.

Source code in lutris/util/wine/wine.py
def use_lutris_runtime(wine_path, force_disable=False):
    """Returns whether to use the Lutris runtime.
    The runtime can be forced to be disabled, otherwise it's disabled
    automatically if Wine is installed system wide.
    """
    if force_disable or runtime.RUNTIME_DISABLED:
        logger.info("Runtime is forced disabled")
        return False
    if WINE_DIR in wine_path:
        logger.debug("%s is provided by Lutris, using runtime", wine_path)
        return True
    if is_installed_systemwide():
        logger.info("Using system wine version, not using runtime")
        return False
    logger.debug("Using Lutris runtime for wine")
    return True

xdgshortcuts

XDG shortcuts handling

create_launcher(game_slug, game_id, game_name, desktop=False, menu=False)

Create a .desktop file.

Source code in lutris/util/xdgshortcuts.py
def create_launcher(game_slug, game_id, game_name, desktop=False, menu=False):
    """Create a .desktop file."""

    desktop_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)
    launcher_content = dedent(
        """
        [Desktop Entry]
        Type=Application
        Name={}
        Icon={}
        Exec=env LUTRIS_SKIP_INIT=1 lutris lutris:rungameid/{}
        Categories=Game
        """.format(game_name, "lutris_{}".format(game_slug), game_id)
    )

    launcher_filename = get_xdg_basename(game_slug, game_id)
    tmp_launcher_path = os.path.join(CACHE_DIR, launcher_filename)
    with open(tmp_launcher_path, "w", encoding='utf-8') as tmp_launcher:
        tmp_launcher.write(launcher_content)
        tmp_launcher.close()
    os.chmod(
        tmp_launcher_path,
        stat.S_IREAD
        | stat.S_IWRITE
        | stat.S_IEXEC
        | stat.S_IRGRP
        | stat.S_IWGRP
        | stat.S_IXGRP,
    )

    if desktop:
        os.makedirs(desktop_dir, exist_ok=True)
        launcher_path = os.path.join(desktop_dir, launcher_filename)
        logger.debug("Creating Desktop icon in %s", launcher_path)
        shutil.copy(tmp_launcher_path, launcher_path)
    if menu:
        menu_path = os.path.join(GLib.get_user_data_dir(), "applications")
        os.makedirs(menu_path, exist_ok=True)
        launcher_path = os.path.join(menu_path, launcher_filename)
        logger.debug("Creating menu launcher in %s", launcher_path)
        shutil.copy(tmp_launcher_path, launcher_path)
    os.remove(tmp_launcher_path)

desktop_launcher_exists(game_slug, game_id)

Return True if there is an existing desktop icon for a game

Source code in lutris/util/xdgshortcuts.py
def desktop_launcher_exists(game_slug, game_id):
    """Return True if there is an existing desktop icon for a game"""
    return system.path_exists(get_launcher_path(game_slug, game_id))

get_launcher_path(game_slug, game_id)

Return the path of a XDG game launcher. When legacy is set, it will return the old path with only the slug, otherwise it will return the path with slug + id

Source code in lutris/util/xdgshortcuts.py
def get_launcher_path(game_slug, game_id):
    """Return the path of a XDG game launcher.
    When legacy is set, it will return the old path with only the slug,
    otherwise it will return the path with slug + id
    """
    desktop_dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP)

    return os.path.join(desktop_dir, get_xdg_basename(game_slug, game_id, base_dir=desktop_dir))

get_menu_launcher_path(game_slug, game_id)

Return the path to a XDG menu launcher, prioritizing legacy paths if they exist

Source code in lutris/util/xdgshortcuts.py
def get_menu_launcher_path(game_slug, game_id):
    """Return the path to a XDG menu launcher, prioritizing legacy paths if
    they exist
    """
    menu_dir = os.path.join(GLib.get_user_data_dir(), "applications")
    return os.path.join(menu_dir, get_xdg_basename(game_slug, game_id, base_dir=menu_dir))

get_xdg_basename(game_slug, game_id, base_dir=None)

Return the filename for .desktop shortcuts

Source code in lutris/util/xdgshortcuts.py
def get_xdg_basename(game_slug, game_id, base_dir=None):
    """Return the filename for .desktop shortcuts"""
    if base_dir:
        # When base dir is provided, lookup possible combinations
        # and return the first match
        for path in [
            "{}.desktop".format(game_slug),
            "{}-{}.desktop".format(game_slug, game_id),
            "net.lutris.{}-{}.desktop".format(game_slug, game_id),
        ]:
            if system.path_exists(os.path.join(base_dir, path)):
                return path

    return "net.lutris.{}-{}.desktop".format(game_slug, game_id)

get_xdg_entry(directory)

Return the path for specific user folders

Source code in lutris/util/xdgshortcuts.py
def get_xdg_entry(directory):
    """Return the path for specific user folders"""
    special_dir = {
        "DESKTOP": GLib.UserDirectory.DIRECTORY_DESKTOP,
        "MUSIC": GLib.UserDirectory.DIRECTORY_MUSIC,
        "PICTURES": GLib.UserDirectory.DIRECTORY_PICTURES,
        "VIDEOS": GLib.UserDirectory.DIRECTORY_VIDEOS,
        "DOCUMENTS": GLib.UserDirectory.DIRECTORY_DOCUMENTS,
    }
    directory = directory.upper()
    if directory not in special_dir.keys():
        raise ValueError(
            directory + " not supported. Only those folders are supported: " + ", ".join(special_dir.keys())
        )
    return GLib.get_user_special_dir(special_dir[directory])

menu_launcher_exists(game_slug, game_id)

Return True if there is an existing application menu entry for a game

Source code in lutris/util/xdgshortcuts.py
def menu_launcher_exists(game_slug, game_id):
    """Return True if there is an existing application menu entry for a game"""
    return system.path_exists(get_menu_launcher_path(game_slug, game_id))

remove_launcher(game_slug, game_id, desktop=False, menu=False)

Remove existing .desktop file.

Source code in lutris/util/xdgshortcuts.py
def remove_launcher(game_slug, game_id, desktop=False, menu=False):
    """Remove existing .desktop file."""
    if desktop:
        launcher_path = get_launcher_path(game_slug, game_id)
        if system.path_exists(launcher_path):
            os.remove(launcher_path)

    if menu:
        menu_path = get_menu_launcher_path(game_slug, game_id)
        if system.path_exists(menu_path):
            os.remove(menu_path)

yaml

Utility functions for YAML handling

read_yaml_from_file(filename)

Read filename and return parsed yaml

Source code in lutris/util/yaml.py
def read_yaml_from_file(filename):
    """Read filename and return parsed yaml"""
    if not path_exists(filename):
        return {}

    with open(filename, "r", encoding='utf-8') as yaml_file:
        try:
            yaml_content = yaml.safe_load(yaml_file) or {}
        except (yaml.scanner.ScannerError, yaml.parser.ParserError):
            logger.error("error parsing file %s", filename)
            yaml_content = {}

    return yaml_content

write_yaml_to_file(config, filepath)

Source code in lutris/util/yaml.py
def write_yaml_to_file(config, filepath):
    yaml_config = yaml.safe_dump(config, default_flow_style=False)
    with open(filepath, "w", encoding='utf-8') as filehandler:
        filehandler.write(yaml_config)